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/Inferencing.scala b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala index 2ebcd96d5bde..7f040ccd2968 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -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 cf3867701c7f..0cc4aaabfc93 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -71,13 +71,6 @@ object ProtoTypes { |constraint was: ${ctx.typerState.constraint} |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 - tvar.instantiate(fromBelow = false) // any direction - - // commit any remaining changes in typer state newctx.typerState.commit() result case _ => testCompat diff --git a/tests/pos/i21981.alt.scala b/tests/pos/i21981.alt.scala new file mode 100644 index 000000000000..0662575757a8 --- /dev/null +++ b/tests/pos/i21981.alt.scala @@ -0,0 +1,24 @@ +trait Ops[F[_], A]: + def map0[B](f0: A => 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 [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.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) } 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) + } +} diff --git a/tests/pos/i21981.scala b/tests/pos/i21981.scala new file mode 100644 index 000000000000..62962c7f1c56 --- /dev/null +++ b/tests/pos/i21981.scala @@ -0,0 +1,24 @@ +trait Ops[F[_], A]: + def map0[B](f0: A => 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[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) }