From 87eff62a9c66158c7b6361babd573ccc211ebcf5 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 3 Mar 2025 12:28:04 +0000 Subject: [PATCH] Revert lambda cleanup (but keep remaining changes) Reverts PR 21466, but keeps the other changes in that PR and in the abandoned attempt that is PR 22031. Fixes issue 21981, by virtue of rebasing. [Cherry-picked 28c1ae3f75e06b82f6e9ec9000292fb93d3db366] --- .../tools/dotc/core/OrderingConstraint.scala | 10 +- .../dotty/tools/dotc/typer/Inferencing.scala | 252 +++++++++--------- .../dotty/tools/dotc/typer/ProtoTypes.scala | 7 - tests/pos/i21981.alt.scala | 24 ++ tests/pos/i21981.contrak.scala | 27 ++ tests/pos/i21981.orig.scala | 29 ++ tests/pos/i21981.scala | 24 ++ 7 files changed, 236 insertions(+), 137 deletions(-) create mode 100644 tests/pos/i21981.alt.scala create mode 100644 tests/pos/i21981.contrak.scala create mode 100644 tests/pos/i21981.orig.scala create mode 100644 tests/pos/i21981.scala diff --git a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala index c9b60ac051ad..78c717539763 100644 --- a/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala +++ b/compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala @@ -263,9 +263,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 @@ -368,7 +368,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 @@ -394,8 +394,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 7acc704fd00d..877c6d36d3e9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inferencing.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inferencing.scala @@ -604,7 +604,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 @@ -628,135 +627,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 9694822bc0d5..89a2cbbfbc21 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -69,13 +69,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) }