From 0c94920163b1752667e0c312a41cbf7c94aa857e Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 26 Nov 2024 14:50:33 +0000 Subject: [PATCH 1/8] Tweak tparam unification to work with lambda cleanup --- .../tools/dotc/core/ConstraintHandling.scala | 13 +++++++-- tests/pos/i21981.scala | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 tests/pos/i21981.scala diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index 04d55475ec60..c34b6a1a1af3 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -436,8 +436,17 @@ trait ConstraintHandling { val level1 = nestingLevel(p1) val level2 = nestingLevel(p2) - val pKept = if level1 <= level2 then p1 else p2 - val pRemoved = if level1 <= level2 then p2 else p1 + val p1Wins = if level1 == level2 then + // If the nesting levels match, then we would prefer to unify to the outer most parameter. + // For instance in pos/i21981, while running `normalizedCompatible` against `.map2`, + // we want to unify to B over K, to allow easily removing K by just instantiating it. + def preferP1(ctx: Context): Boolean = + val c = ctx.typerState.constraint + !c.contains(p2) || c.contains(p1) && preferP1(ctx.outer) + preferP1(ctx) + else level1 <= level2 + val pKept = if p1Wins then p1 else p2 + val pRemoved = if p1Wins then p2 else p1 val down = constraint.exclusiveLower(p2, p1) val up = constraint.exclusiveUpper(p1, p2) diff --git a/tests/pos/i21981.scala b/tests/pos/i21981.scala new file mode 100644 index 000000000000..eae3debbd800 --- /dev/null +++ b/tests/pos/i21981.scala @@ -0,0 +1,28 @@ +trait Ops[F[_], A]: + def map0[B](f0: A => B): F[B] = ??? + +trait Functor1[G[_]] + +trait Functor2[H[_]]: + extension [C](hc: H[C]) + def map2[D](f1: C => D): H[D] + +trait Ref[I[_], +E] + +final class Cov[+F] + +class Test: + given [J[_]](using J: Functor1[J]): Functor2[J] with + extension [K](jk: J[K]) + def map2[L](f2: K => L): J[L] = ??? + + def t1[ + M[_[t]], + N[_], + ](using N: Functor1[N]): Unit = + + val x3: Ops[N, M[[t] =>> Ref[N, t]]] = ??? + + val x2: N[(M[N], M[[t] =>> Ref[N, t]])] = x3 + .map0 { refs => (???, refs) } + .map2 { case (not, refs) => (???, refs) } From 53bbae824ad617b5ebee54b6099679e6e256d57d Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 30 Jan 2025 15:26:47 +0000 Subject: [PATCH 2/8] Tweak lambda/tvar cleanup, to avoid mis-instantiating --- .../tools/dotc/core/ConstraintHandling.scala | 13 ++------- .../dotty/tools/dotc/typer/ProtoTypes.scala | 10 ++++++- tests/pos/i21981.alt.scala | 28 ++++++++++++++++++ tests/pos/i21981.orig.scala | 29 +++++++++++++++++++ 4 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 tests/pos/i21981.alt.scala create mode 100644 tests/pos/i21981.orig.scala diff --git a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala index c34b6a1a1af3..04d55475ec60 100644 --- a/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala +++ b/compiler/src/dotty/tools/dotc/core/ConstraintHandling.scala @@ -436,17 +436,8 @@ trait ConstraintHandling { val level1 = nestingLevel(p1) val level2 = nestingLevel(p2) - val p1Wins = if level1 == level2 then - // If the nesting levels match, then we would prefer to unify to the outer most parameter. - // For instance in pos/i21981, while running `normalizedCompatible` against `.map2`, - // we want to unify to B over K, to allow easily removing K by just instantiating it. - def preferP1(ctx: Context): Boolean = - val c = ctx.typerState.constraint - !c.contains(p2) || c.contains(p1) && preferP1(ctx.outer) - preferP1(ctx) - else level1 <= level2 - val pKept = if p1Wins then p1 else p2 - val pRemoved = if p1Wins then p2 else p1 + val pKept = if level1 <= level2 then p1 else p2 + val pRemoved = if level1 <= level2 then p2 else p1 val down = constraint.exclusiveLower(p2, p1) val up = constraint.exclusiveUpper(p1, p2) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index cf3867701c7f..a40498c1c846 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -75,7 +75,15 @@ object ProtoTypes { for tvar <- newctx.typerState.ownedVars do inContext(newctx): if !tvar.isInstantiated then - tvar.instantiate(fromBelow = false) // any direction + // Filter out any tvar that instantiating would further constrain the current constraint + // Similar to filterByDeps in interpolateTypeVars. + val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) + val aboveOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) + val belowOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) + if aboveOK then + tvar.instantiate(fromBelow = true) + else if belowOK then + tvar.instantiate(fromBelow = false) // commit any remaining changes in typer state newctx.typerState.commit() diff --git a/tests/pos/i21981.alt.scala b/tests/pos/i21981.alt.scala new file mode 100644 index 000000000000..63b70a55b101 --- /dev/null +++ b/tests/pos/i21981.alt.scala @@ -0,0 +1,28 @@ +trait Ops[F[_], A]: + def map0[B](f0: A => B): F[B] = ??? + +trait Functor1[G[_]] + +trait Functor2[H[_]]: + extension [C1, C2](hc: H[(C1, C2)]) + def map2[D](f1: (C1, C2) => D): H[D] + +trait Ref[I[_], +E] + +final class Cov[+F] + +class Test: + given [J[_]](using J: Functor1[J]): Functor2[J] with + extension [K1, K2](jk: J[(K1, K2)]) + def map2[L](f2: (K1, K2) => L): J[L] = ??? + + def t1[ + M[_[t]], + N[_], + ](using N: Functor1[N]): Unit = + + val x3: Ops[N, M[[t] =>> Ref[N, t]]] = ??? + + val x2: N[(M[N], M[[t] =>> Ref[N, t]])] = x3 + .map0 { refs => (???, refs) } + .map2 { case (not, refs) => (???, refs) } diff --git a/tests/pos/i21981.orig.scala b/tests/pos/i21981.orig.scala new file mode 100644 index 000000000000..c02eb4848649 --- /dev/null +++ b/tests/pos/i21981.orig.scala @@ -0,0 +1,29 @@ +object internal: + trait Functor[F[_]] { + extension [T](ft: F[T]) def map[T1](f: T => T1): F[T1] + } + +object cats: + trait Functor[F[_]] + object Functor: + trait Ops[F[_], A]: + def map[B](f: A => B): F[B] = ??? + def toAllFunctorOps[F[_], A](target: F[A])(using Functor[F]): Ops[F, A] = ??? + +given [F[_]](using cf: cats.Functor[F]): internal.Functor[F] with { + extension [T](ft: F[T]) def map[T1](f: T => T1): F[T1] = ??? +} + +trait Ref[F[_], +T] +class MemoizingEvaluator[Input[_[_]], Output[_[_]], F[_]: cats.Functor] { + type OptionRef[T] = Ref[F, Option[T]] + + def sequence[CaseClass[_[_]], G[_], H[_]](instance: CaseClass[[t] =>> G[H[t]]]): G[CaseClass[H]] = ??? + def collectValues(input: Input[F]): F[(Input[F], Input[OptionRef])] = { + val refsF: Input[[t] =>> F[OptionRef[t]]] = ??? + for { + refs <- cats.Functor.toAllFunctorOps(sequence[Input, F, OptionRef](refsF)) + updating = ??? + } yield (updating, refs) + } +} From 4ca60086c3fcaadd91316496deeadff5959b5cd2 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 19 Feb 2025 10:09:03 +0000 Subject: [PATCH 3/8] Fix typo in tvar instantiation logic --- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index a40498c1c846..d069a9e7784f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -80,9 +80,9 @@ object ProtoTypes { val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) val aboveOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) val belowOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) - if aboveOK then + if belowOK then tvar.instantiate(fromBelow = true) - else if belowOK then + else if aboveOK then tvar.instantiate(fromBelow = false) // commit any remaining changes in typer state From 4c9f92accdba41420e435cae10683bbd90b62a74 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 20 Feb 2025 10:43:13 +0000 Subject: [PATCH 4/8] Strengthen lambda cleanup restrictions --- .../dotty/tools/dotc/typer/ProtoTypes.scala | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index d069a9e7784f..ae0c1fdc41d1 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -77,13 +77,22 @@ object ProtoTypes { if !tvar.isInstantiated then // Filter out any tvar that instantiating would further constrain the current constraint // Similar to filterByDeps in interpolateTypeVars. + // Also, filter out any tvar that is the instantiation another tvar + // (that we're not also trying to instantiate) + // For example, in tests/pos/i21981.scala + // when testing the compatibility of `.map2[?K]` on receiver `map0[?B]` + // the tvars for B and K unified, instantiating `B := K`, + // so we can't instantiate away `K` as it would incorrectly define `B`. val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) - val aboveOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) - val belowOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) - if belowOK then - tvar.instantiate(fromBelow = true) - else if aboveOK then + var isInst = false + ctx.typerState.constraint.foreachTypeVar: tvar1 => + isInst ||= !excluded.contains(tvar1) && tvar1.instanceOpt == tvar + val aboveOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) + val belowOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) + if aboveOK then tvar.instantiate(fromBelow = false) + else if belowOK then + tvar.instantiate(fromBelow = true) // commit any remaining changes in typer state newctx.typerState.commit() From 405365619f3cbd49be402f4a6cac85766ec50f78 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 24 Feb 2025 10:25:22 +0000 Subject: [PATCH 5/8] Fix typos & redundant test code --- .../src/dotty/tools/dotc/core/OrderingConstraint.scala | 10 +++++----- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 2 +- tests/pos/i21981.alt.scala | 6 +----- tests/pos/i21981.scala | 6 +----- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 8256a3cdbab1..150c39aa8e13 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -265,9 +265,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, private var coDeps: ReverseDeps = SimpleIdentityMap.empty /** A map that associates type parameters of this constraint with all other type - * parameters that refer to them in their bounds covariantly, such that, if the + * parameters that refer to them in their bounds contravariantly, such that, if the * type parameter is instantiated to a smaller type, the constraint would be narrowed. - * (i.e. solution set changes other than simply being made larger). + * (i.e. solution set changes other than simply being made smaller). */ private var contraDeps: ReverseDeps = SimpleIdentityMap.empty @@ -370,7 +370,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, /** Adjust reverse dependencies of all type parameters referenced by `bound` * @param isLower `bound` is a lower bound - * @param add if true, add referenced variables to dependencoes, otherwise drop them. + * @param add if true, add referenced variables to dependencies, otherwise drop them. */ def adjustReferenced(bound: Type, isLower: Boolean, add: Boolean) = adjuster.variance = if isLower then 1 else -1 @@ -396,8 +396,8 @@ class OrderingConstraint(private val boundsMap: ParamBounds, } case _ => false - /** Add or remove depenencies referenced in `bounds`. - * @param add if true, dependecies are added, otherwise they are removed + /** Add or remove dependencies referenced in `bounds`. + * @param add if true, dependencies are added, otherwise they are removed */ def adjustBounds(bounds: TypeBounds, add: Boolean) = adjustReferenced(bounds.lo, isLower = true, add) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index ae0c1fdc41d1..ac9b6f9d7188 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -77,7 +77,7 @@ object ProtoTypes { if !tvar.isInstantiated then // Filter out any tvar that instantiating would further constrain the current constraint // Similar to filterByDeps in interpolateTypeVars. - // Also, filter out any tvar that is the instantiation another tvar + // Also, filter out any tvar that is in the instantiation of another tvar // (that we're not also trying to instantiate) // For example, in tests/pos/i21981.scala // when testing the compatibility of `.map2[?K]` on receiver `map0[?B]` diff --git a/tests/pos/i21981.alt.scala b/tests/pos/i21981.alt.scala index 63b70a55b101..0662575757a8 100644 --- a/tests/pos/i21981.alt.scala +++ b/tests/pos/i21981.alt.scala @@ -3,14 +3,10 @@ trait Ops[F[_], A]: trait Functor1[G[_]] -trait Functor2[H[_]]: - extension [C1, C2](hc: H[(C1, C2)]) - def map2[D](f1: (C1, C2) => D): H[D] +trait Functor2[H[_]] trait Ref[I[_], +E] -final class Cov[+F] - class Test: given [J[_]](using J: Functor1[J]): Functor2[J] with extension [K1, K2](jk: J[(K1, K2)]) diff --git a/tests/pos/i21981.scala b/tests/pos/i21981.scala index eae3debbd800..62962c7f1c56 100644 --- a/tests/pos/i21981.scala +++ b/tests/pos/i21981.scala @@ -3,14 +3,10 @@ trait Ops[F[_], A]: trait Functor1[G[_]] -trait Functor2[H[_]]: - extension [C](hc: H[C]) - def map2[D](f1: C => D): H[D] +trait Functor2[H[_]] trait Ref[I[_], +E] -final class Cov[+F] - class Test: given [J[_]](using J: Functor1[J]): Functor2[J] with extension [K](jk: J[K]) From 302a3822fb3a9d110291381edcda1b3f3386d0c4 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 24 Feb 2025 10:40:11 +0000 Subject: [PATCH 6/8] Keep deps so "dependsOn" is correct --- .../tools/dotc/core/OrderingConstraint.scala | 4 ++- .../dotty/tools/dotc/typer/ProtoTypes.scala | 13 ++------- tests/pos/i21981.contrak.scala | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 tests/pos/i21981.contrak.scala diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 150c39aa8e13..1fb680209ce9 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -419,7 +419,9 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case prevEntry: TypeBounds => adjustBounds(prevEntry, add = false) case _ => - dropDeps(srcParam) // srcParam is instantiated, so its dependencies can be dropped + if entry != null && entry.exists then + // srcParam is instantiated, but keep dependencies to respond to "dependsOn" + adjustBounds(entry.bounds, add = true) this end adjustDeps diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index ac9b6f9d7188..a00d0edab47f 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -77,18 +77,9 @@ object ProtoTypes { if !tvar.isInstantiated then // Filter out any tvar that instantiating would further constrain the current constraint // Similar to filterByDeps in interpolateTypeVars. - // Also, filter out any tvar that is in the instantiation of another tvar - // (that we're not also trying to instantiate) - // For example, in tests/pos/i21981.scala - // when testing the compatibility of `.map2[?K]` on receiver `map0[?B]` - // the tvars for B and K unified, instantiating `B := K`, - // so we can't instantiate away `K` as it would incorrectly define `B`. val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) - var isInst = false - ctx.typerState.constraint.foreachTypeVar: tvar1 => - isInst ||= !excluded.contains(tvar1) && tvar1.instanceOpt == tvar - val aboveOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) - val belowOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) + val aboveOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) + val belowOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) if aboveOK then tvar.instantiate(fromBelow = false) else if belowOK then diff --git a/tests/pos/i21981.contrak.scala b/tests/pos/i21981.contrak.scala new file mode 100644 index 000000000000..6ce64e6d3ddf --- /dev/null +++ b/tests/pos/i21981.contrak.scala @@ -0,0 +1,27 @@ +case class Inv[T](x: T) +class Contra[-ContraParam](x: ContraParam) + +trait Ops[F[_], A]: + def map0[B](f0: A => Contra[B]): F[B] = ??? + +trait Functor1[G[_]] + +trait Functor2[H[_]] + +trait Ref[I[_], +E] + +class Test: + given [J[_]](using J: Functor1[J]): Functor2[J] with + extension [K](jk: J[Contra[K]]) + def map2[L](f2: K => L): J[L] = ??? + + def t1[ + M[_[t]], + N[_], + ](using N: Functor1[N]): Unit = + + val x3: Ops[N, M[[t] =>> Ref[N, t]]] = ??? + + val x2: N[(M[N], M[[t] =>> Ref[N, t]])] = x3 + .map0 { refs => Contra[Contra[(Nothing, M[[t] =>> Ref[N, t]])]](???) } + .map2 { case (not, refs) => (???, refs) } From 68045d395a42f2fdae0b2b4d2ad4f9f513e65cfe Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 27 Feb 2025 08:56:12 +0000 Subject: [PATCH 7/8] Revert "keep deps" code change --- .../dotty/tools/dotc/core/OrderingConstraint.scala | 4 +--- .../src/dotty/tools/dotc/typer/ProtoTypes.scala | 13 +++++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index 1fb680209ce9..150c39aa8e13 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -419,9 +419,7 @@ class OrderingConstraint(private val boundsMap: ParamBounds, case prevEntry: TypeBounds => adjustBounds(prevEntry, add = false) case _ => - if entry != null && entry.exists then - // srcParam is instantiated, but keep dependencies to respond to "dependsOn" - adjustBounds(entry.bounds, add = true) + dropDeps(srcParam) // srcParam is instantiated, so its dependencies can be dropped this end adjustDeps diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index a00d0edab47f..ac9b6f9d7188 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -77,9 +77,18 @@ object ProtoTypes { if !tvar.isInstantiated then // Filter out any tvar that instantiating would further constrain the current constraint // Similar to filterByDeps in interpolateTypeVars. + // Also, filter out any tvar that is in the instantiation of another tvar + // (that we're not also trying to instantiate) + // For example, in tests/pos/i21981.scala + // when testing the compatibility of `.map2[?K]` on receiver `map0[?B]` + // the tvars for B and K unified, instantiating `B := K`, + // so we can't instantiate away `K` as it would incorrectly define `B`. val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) - val aboveOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) - val belowOK = !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) + var isInst = false + ctx.typerState.constraint.foreachTypeVar: tvar1 => + isInst ||= !excluded.contains(tvar1) && tvar1.instanceOpt == tvar + val aboveOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) + val belowOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) if aboveOK then tvar.instantiate(fromBelow = false) else if belowOK then From d28109f5f86d50b1891b64e85e43863ef6510290 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 27 Feb 2025 11:47:47 +0000 Subject: [PATCH 8/8] Change `variances` and reuse interpolateTypeVars --- .../dotty/tools/dotc/typer/Inferencing.scala | 254 +++++++++--------- .../dotty/tools/dotc/typer/ProtoTypes.scala | 24 +- 2 files changed, 131 insertions(+), 147 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 2ebcd96d5bde..386922192cb2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -586,7 +586,7 @@ object Inferencing { if (vmap1 eq vmap) vmap else propagate(vmap1) } - propagate(accu(accu(VarianceMap.empty, tp), pt.finalResultType)) + propagate(accu(accu(VarianceMap.empty, tp), pt)) } /** Run the transformation after dealiasing but return the original type if it was a no-op. */ @@ -671,7 +671,6 @@ trait Inferencing { this: Typer => // This is needed because it could establish singleton type upper bounds. See i2998.scala. val tp = tree.tpe.widen - val vs = variances(tp, pt) // Avoid interpolating variables occurring in tree's type if typerstate has unreported errors. // Reason: The errors might reflect unsatisfiable constraints. In that @@ -695,135 +694,138 @@ trait Inferencing { this: Typer => // val y: List[List[String]] = List(List(1)) if state.reporter.hasUnreportedErrors then return tree - def constraint = state.constraint - - trace(i"interpolateTypeVars($tree: ${tree.tpe}, $pt, $qualifying)", typr, (_: Any) => i"$qualifying\n$constraint\n${ctx.gadt}") { - //println(i"$constraint") - //println(i"${ctx.gadt}") - - /** Values of this type report type variables to instantiate with variance indication: - * +1 variable appears covariantly, can be instantiated from lower bound - * -1 variable appears contravariantly, can be instantiated from upper bound - * 0 variable does not appear at all, can be instantiated from either bound - */ - type ToInstantiate = List[(TypeVar, Int)] - - val toInstantiate: ToInstantiate = - val buf = new mutable.ListBuffer[(TypeVar, Int)] - for tvar <- qualifying do - if !tvar.isInstantiated && constraint.contains(tvar) && tvar.nestingLevel >= ctx.nestingLevel then - constrainIfDependentParamRef(tvar, tree) - if !tvar.isInstantiated then - // isInstantiated needs to be checked again, since previous interpolations could already have - // instantiated `tvar` through unification. - val v = vs.computedVariance(tvar) - if v == null then buf += ((tvar, 0)) - else if v.intValue != 0 then buf += ((tvar, v.intValue)) - else comparing(cmp => - if !cmp.levelOK(tvar.nestingLevel, ctx.nestingLevel) then - // Invariant: The type of a tree whose enclosing scope is level - // N only contains type variables of level <= N. - typr.println(i"instantiate nonvariant $tvar of level ${tvar.nestingLevel} to a type variable of level <= ${ctx.nestingLevel}, $constraint") - cmp.atLevel(ctx.nestingLevel, tvar.origin) - else - typr.println(i"no interpolation for nonvariant $tvar in $state") - ) - // constrainIfDependentParamRef could also have instantiated tvars added to buf before the check - buf.filterNot(_._1.isInstantiated).toList - end toInstantiate - - def typeVarsIn(xs: ToInstantiate): TypeVars = - xs.foldLeft(SimpleIdentitySet.empty: TypeVars)((tvs, tvi) => tvs + tvi._1) - - /** Filter list of proposed instantiations so that they don't constrain further - * the current constraint. - */ - def filterByDeps(tvs0: ToInstantiate): ToInstantiate = - val excluded = // ignore dependencies from other variables that are being instantiated - typeVarsIn(tvs0) - def step(tvs: ToInstantiate): ToInstantiate = tvs match - case tvs @ (hd @ (tvar, v)) :: tvs1 => - def aboveOK = !constraint.dependsOn(tvar, excluded, co = true) - def belowOK = !constraint.dependsOn(tvar, excluded, co = false) - if v == 0 && !aboveOK then - step((tvar, 1) :: tvs1) - else if v == 0 && !belowOK then - step((tvar, -1) :: tvs1) - else if v == -1 && !aboveOK || v == 1 && !belowOK then - typr.println(i"drop $tvar, $v in $tp, $pt, qualifying = ${qualifying.toList}, tvs0 = ${tvs0.toList}%, %, excluded = ${excluded.toList}, $constraint") - step(tvs1) - else // no conflict, keep the instantiation proposal - tvs.derivedCons(hd, step(tvs1)) - case Nil => - Nil - val tvs1 = step(tvs0) - if tvs1 eq tvs0 then tvs1 - else filterByDeps(tvs1) // filter again with smaller excluded set - end filterByDeps - - /** Instantiate all type variables in `tvs` in the indicated directions, - * as described in the doc comment of `ToInstantiate`. - * If a type variable A is instantiated from below, and there is another - * type variable B in `buf` that is known to be smaller than A, wait and - * instantiate all other type variables before trying to instantiate A again. - * Dually, wait instantiating a type variable from above as long as it has - * upper bounds in `buf`. - * - * This is done to avoid loss of precision when forming unions. An example - * is in i7558.scala: - * - * type Tr[+V1, +O1 <: V1] - * extension [V2, O2 <: V2](tr: Tr[V2, O2]) def sl: Tr[V2, O2] = ??? - * def as[V3, O3 <: V3](tr: Tr[V3, O3]) : Tr[V3, O3] = tr.sl - * - * Here we interpolate at some point V2 and O2 given the constraint - * - * V2 >: V3, O2 >: O3, O2 <: V2 - * - * where O3 and V3 are type refs with O3 <: V3. - * If we interpolate V2 first to V3 | O2, the widenUnion algorithm will - * instantiate O2 to V3, leading to the final constraint - * - * V2 := V3, O2 := V3 - * - * But if we instantiate O2 first to O3, and V2 next to V3, we get the - * more flexible instantiation - * - * V2 := V3, O2 := O3 - */ - def doInstantiate(tvs: ToInstantiate): Unit = - - /** Try to instantiate `tvs`, return any suspended type variables */ - def tryInstantiate(tvs: ToInstantiate): ToInstantiate = tvs match - case (hd @ (tvar, v)) :: tvs1 => - val fromBelow = v == 1 || (v == 0 && tvar.hasLowerBound) - typr.println( - i"interpolate${if v == 0 then " non-occurring" else ""} $tvar in $state in $tree: $tp, fromBelow = $fromBelow, $constraint") - if tvar.isInstantiated then - tryInstantiate(tvs1) - else - val suspend = tvs1.exists{ (following, _) => - if fromBelow - then constraint.isLess(following.origin, tvar.origin) - else constraint.isLess(tvar.origin, following.origin) - } - if suspend then - typr.println(i"suspended: $hd") - hd :: tryInstantiate(tvs1) - else - tvar.instantiate(fromBelow) - tryInstantiate(tvs1) - case Nil => Nil - if tvs.nonEmpty then doInstantiate(tryInstantiate(tvs)) - end doInstantiate - - doInstantiate(filterByDeps(toInstantiate)) - } + instantiateTypeVars(tp, pt, qualifying, tree) } end if tree end interpolateTypeVars + def instantiateTypeVars(tp: Type, pt: Type, qualifying: List[TypeVar], tree: Tree = EmptyTree)(using Context): Unit = + trace(i"instantiateTypeVars($tp, $pt, $qualifying, $tree)", typr): + val state = ctx.typerState + def constraint = state.constraint + + val vs = variances(tp, pt) + + /** Values of this type report type variables to instantiate with variance indication: + * +1 variable appears covariantly, can be instantiated from lower bound + * -1 variable appears contravariantly, can be instantiated from upper bound + * 0 variable does not appear at all, can be instantiated from either bound + */ + type ToInstantiate = List[(TypeVar, Int)] + + val toInstantiate: ToInstantiate = + val buf = new mutable.ListBuffer[(TypeVar, Int)] + for tvar <- qualifying do + if !tvar.isInstantiated && constraint.contains(tvar) && tvar.nestingLevel >= ctx.nestingLevel then + constrainIfDependentParamRef(tvar, tree) + if !tvar.isInstantiated then + // isInstantiated needs to be checked again, since previous interpolations could already have + // instantiated `tvar` through unification. + val v = vs.computedVariance(tvar) + if v == null then buf += ((tvar, 0)) + else if v.intValue != 0 then buf += ((tvar, v.intValue)) + else comparing(cmp => + if !cmp.levelOK(tvar.nestingLevel, ctx.nestingLevel) then + // Invariant: The type of a tree whose enclosing scope is level + // N only contains type variables of level <= N. + typr.println(i"instantiate nonvariant $tvar of level ${tvar.nestingLevel} to a type variable of level <= ${ctx.nestingLevel}, $constraint") + cmp.atLevel(ctx.nestingLevel, tvar.origin) + else + typr.println(i"no interpolation for nonvariant $tvar in $state") + ) + // constrainIfDependentParamRef could also have instantiated tvars added to buf before the check + buf.filterNot(_._1.isInstantiated).toList + end toInstantiate + + def typeVarsIn(xs: ToInstantiate): TypeVars = + xs.foldLeft(SimpleIdentitySet.empty: TypeVars)((tvs, tvi) => tvs + tvi._1) + + /** Filter list of proposed instantiations so that they don't constrain further + * the current constraint. + */ + def filterByDeps(tvs0: ToInstantiate): ToInstantiate = + val excluded = // ignore dependencies from other variables that are being instantiated + typeVarsIn(tvs0) + def step(tvs: ToInstantiate): ToInstantiate = tvs match + case tvs @ (hd @ (tvar, v)) :: tvs1 => + def aboveOK = !constraint.dependsOn(tvar, excluded, co = true) + def belowOK = !constraint.dependsOn(tvar, excluded, co = false) + if v == 0 && !aboveOK then + step((tvar, 1) :: tvs1) + else if v == 0 && !belowOK then + step((tvar, -1) :: tvs1) + else if v == -1 && !aboveOK || v == 1 && !belowOK then + typr.println(i"drop $tvar, $v in $tp, $pt, qualifying = ${qualifying.toList}, tvs0 = ${tvs0.toList}%, %, excluded = ${excluded.toList}, $constraint") + step(tvs1) + else // no conflict, keep the instantiation proposal + tvs.derivedCons(hd, step(tvs1)) + case Nil => + Nil + val tvs1 = step(tvs0) + if tvs1 eq tvs0 then tvs1 + else filterByDeps(tvs1) // filter again with smaller excluded set + end filterByDeps + + /** Instantiate all type variables in `tvs` in the indicated directions, + * as described in the doc comment of `ToInstantiate`. + * If a type variable A is instantiated from below, and there is another + * type variable B in `buf` that is known to be smaller than A, wait and + * instantiate all other type variables before trying to instantiate A again. + * Dually, wait instantiating a type variable from above as long as it has + * upper bounds in `buf`. + * + * This is done to avoid loss of precision when forming unions. An example + * is in i7558.scala: + * + * type Tr[+V1, +O1 <: V1] + * extension [V2, O2 <: V2](tr: Tr[V2, O2]) def sl: Tr[V2, O2] = ??? + * def as[V3, O3 <: V3](tr: Tr[V3, O3]) : Tr[V3, O3] = tr.sl + * + * Here we interpolate at some point V2 and O2 given the constraint + * + * V2 >: V3, O2 >: O3, O2 <: V2 + * + * where O3 and V3 are type refs with O3 <: V3. + * If we interpolate V2 first to V3 | O2, the widenUnion algorithm will + * instantiate O2 to V3, leading to the final constraint + * + * V2 := V3, O2 := V3 + * + * But if we instantiate O2 first to O3, and V2 next to V3, we get the + * more flexible instantiation + * + * V2 := V3, O2 := O3 + */ + def doInstantiate(tvs: ToInstantiate): Unit = + + /** Try to instantiate `tvs`, return any suspended type variables */ + def tryInstantiate(tvs: ToInstantiate): ToInstantiate = tvs match + case (hd @ (tvar, v)) :: tvs1 => + val fromBelow = v == 1 || (v == 0 && tvar.hasLowerBound) + typr.println( + i"interpolate${if v == 0 then " non-occurring" else ""} $tvar in $state in $tree: $tp, fromBelow = $fromBelow, $constraint") + if tvar.isInstantiated then + tryInstantiate(tvs1) + else + val suspend = tvs1.exists{ (following, _) => + if fromBelow + then constraint.isLess(following.origin, tvar.origin) + else constraint.isLess(tvar.origin, following.origin) + } + if suspend then + typr.println(i"suspended: $hd") + hd :: tryInstantiate(tvs1) + else + tvar.instantiate(fromBelow) + tryInstantiate(tvs1) + case Nil => Nil + if tvs.nonEmpty then doInstantiate(tryInstantiate(tvs)) + end doInstantiate + + doInstantiate(filterByDeps(toInstantiate)) + end instantiateTypeVars + /** If `tvar` represents a parameter of a dependent method type in the current `call` * approximate it from below with the type of the actual argument. Skolemize that * type if necessary to make it a Singleton. diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index ac9b6f9d7188..95735f8d89de 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -72,27 +72,9 @@ object ProtoTypes { |constraint now: ${newctx.typerState.constraint}""") if result && (ctx.typerState.constraint ne newctx.typerState.constraint) then // Remove all type lambdas and tvars introduced by testCompat - for tvar <- newctx.typerState.ownedVars do - inContext(newctx): - if !tvar.isInstantiated then - // Filter out any tvar that instantiating would further constrain the current constraint - // Similar to filterByDeps in interpolateTypeVars. - // Also, filter out any tvar that is in the instantiation of another tvar - // (that we're not also trying to instantiate) - // For example, in tests/pos/i21981.scala - // when testing the compatibility of `.map2[?K]` on receiver `map0[?B]` - // the tvars for B and K unified, instantiating `B := K`, - // so we can't instantiate away `K` as it would incorrectly define `B`. - val excluded = ctx.typerState.ownedVars.filter(!_.isInstantiated) - var isInst = false - ctx.typerState.constraint.foreachTypeVar: tvar1 => - isInst ||= !excluded.contains(tvar1) && tvar1.instanceOpt == tvar - val aboveOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = true) - val belowOK = !isInst && !ctx.typerState.constraint.dependsOn(tvar, excluded, co = false) - if aboveOK then - tvar.instantiate(fromBelow = false) - else if belowOK then - tvar.instantiate(fromBelow = true) + inContext(newctx): + val tvars = ctx.typerState.ownedVars.toList + ctx.typer.instantiateTypeVars(poly, pt, tvars) // commit any remaining changes in typer state newctx.typerState.commit()