Skip to content

Tweak tparam unification to work with lambda cleanup #22031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,24 @@ 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.
// 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`,
Copy link
Member

@smarter smarter Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check tvar1.instanceOpt == tvar accounts for that, but it doesn't account for B := Foo[K]. Here's a test case with B := Contra[K]:

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) }

Now this could probably be fixed by checking tvar.occursIn(tvar1.instanceOpt) but at this point I'd started getting worried about time complexity: we're already inside two nested loops over all type variables and now we're traversing a type (in fact this is the traversal that dependsOn avoids for uninstantiated type variables).

// 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)

// commit any remaining changes in typer state
newctx.typerState.commit()
Expand Down
28 changes: 28 additions & 0 deletions tests/pos/i21981.alt.scala
Original file line number Diff line number Diff line change
@@ -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) }
29 changes: 29 additions & 0 deletions tests/pos/i21981.orig.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
28 changes: 28 additions & 0 deletions tests/pos/i21981.scala
Original file line number Diff line number Diff line change
@@ -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) }
Loading