scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.83k stars 1.05k forks source link

Anonymous trait mixin breaks inline match + erasedValue + summonInline #17222

Open kavedaa opened 1 year ago

kavedaa commented 1 year ago

Compiler version

3.2.2, 3.3.0-RC3

Minimized code

//> using scala "3.3.0-RC3"

import scala.deriving.Mirror
import scala.compiletime.*

trait Reader[-In, Out]

trait A:
  type T
  type F[X]
  type Q = F[T]

object Reader:

  given [X]: Reader[A { type Q = X }, X] with {}    

  type Map2[Tup1 <: Tuple, Tup2 <: Tuple, F[_, _]] <: Tuple = (Tup1, Tup2) match
    case (h1 *: t1, h2 *: t2) => F[h1, h2] *: Map2[t1, t2, F]
    case (EmptyTuple, EmptyTuple) => EmptyTuple    

  inline given productReader[In <: Product, Out <: Product](using mi: Mirror.ProductOf[In])(using mo: Mirror.ProductOf[Out]): Reader[In, Out] = 
    summonAll[Map2[mi.MirroredElemTypes, mo.MirroredElemTypes, Reader]]
    ???

object Test:

  trait B[X] extends A:
    type T = X

  trait C extends A:
    type F[X] = X

  val bc = new B[Int] with C

  summon[Reader[(bc.type, bc.type), (Int, Int)]]    // fails

Output

Compiling project (Scala 3.3.0-RC3, JVM)
[error] .\fails.scala:35:49: No given instance of type Reader[(Test.bc : Test.B[Int] & Test.C), Int] was found.
[error] I found:
[error]
[error]     Reader.productReader[In, Out](/* missing */summon[deriving.Mirror.ProductOf[In]]
[error]       )
[error]
[error] But Failed to synthesize an instance of type deriving.Mirror.ProductOf[In]: class Nothing is not a generic product because it is not a case class.
[error]   summon[Reader[(bc.type, bc.type), (Int, Int)]]    // fails
[error]                                                 ^
Error compiling project (Scala 3.3.0-RC3, JVM)
Compilation failed

Expectation

Successful compilation

kavedaa commented 1 year ago

If the last couple of lines are replaced with this, it works:

trait D[X] extends B[X] with C

val d = new D[Int] {}

summon[Reader[(d.type, d.type), (Int, Int)]]    // works
bishabosha commented 1 year ago

this issue is incorrectly titled, in fact the mirror synthesis works fine (can be observed with -Xprint:typer), what is failing is summonAll[(Reader[bc.type, Int], Reader[bc.type, Int])], see the following code, where I manually inline Reader. productReader:

//> using scala "3.3.0-RC3"

import scala.compiletime.*

trait Reader[-In, Out]

trait A:
  type T
  type F[X]
  type Q = F[T]

object Reader:

  given [X]: Reader[A { type Q = X }, X] with {}

object Test:

  trait B[X] extends A:
    type T = X

  trait C extends A:
    type F[X] = X

  trait D[X] extends B[X] with C

  val d = new D[Int] {}
  val bc = new B[Int] with C

  summonAll[(Reader[d.type, Int], Reader[d.type, Int])] // works
  summonAll[(Reader[bc.type, Int], Reader[bc.type, Int])] // error
  summonInline[Reader[d.type, Int]] // works
  summonInline[Reader[bc.type, Int]] // works??

so it appears the problem is summonAll

bishabosha commented 1 year ago

If I reimplement summonAll as summonOne, with a single case, then we get the same problem, so is the issue inline match with erasedValue?

object Test:
  // ...

  case class Box[T]()

  /** compiletime.summonAll, but with one case */
  inline def summonOne[T <: Box[?]]: T =
    val res =
      inline erasedValue[T] match
        case _: Box[t] => summonInline[t]
      end match
    Box(res).asInstanceOf[T]
  end summonOne

  summonOne[Box[Reader[d.type, Int]]] // works
  summonOne[Box[Reader[bc.type, Int]]] // errors
kavedaa commented 4 months ago

Here is a complete minimized example. There are many ways to make it compile, but only one way to make it fail:

//> using scala 3.3.3

import scala.compiletime.*

trait Reader[-In, Out]

trait A:
  type T
  type F[X]
  type Q = F[T]

given [X]: Reader[A { type Q = X }, X] with {}    

case class Box[T](x: T)

/** compiletime.summonAll, but with one case */
inline def summonOne[T]: T =
  val res =
    inline erasedValue[T] match
      case _: Box[t] => summonInline[t]
    end match
  Box(res).asInstanceOf[T]
end summonOne

@main def main = 

  trait B[X] extends A:
    type T = X

  trait C extends A:
    type F[X] = X

  val bc = new B[Int] with C

  summonOne[Box[Reader[bc.type, Int]]] // errors

  val bc2: A { type Q = Int } = new B[Int] with C

  summonOne[Box[Reader[bc2.type, Int]]] // works

  object BC extends B[Int] with C

  summonOne[Box[Reader[BC.type, Int]]] // works

  val a = new A:
    type T = Int
    type F[X] = X

  summonOne[Box[Reader[a.type, Int]]] // works

  val b = new B[Int]:
    type F[X] = X

  summonOne[Box[Reader[b.type, Int]]] // works

  val ac = new A with C:
    type T = Int

  summonOne[Box[Reader[ac.type, Int]]] // works

  trait D[X] extends B[X] with C
  val d = new D[Int] {}

  summonOne[Box[Reader[d.type, Int]]] // works
jchyb commented 3 months ago

Spent some time investigating this, further minimization:

//> using scala 3.3.3
import scala.compiletime.*

trait Reader[-In, Out]

trait A:
  type T
  type F[X]
  type Q = F[T]

given [X]: Reader[A { type Q = X }, X] with {}    

case class Box[T](x: T)

inline def summonOne[T]: T =
  summonInline[T]
end summonOne

@main def main = 
  trait B[X] extends A:
    type T = X
  trait C extends A:
    type F[X] = X

  val bc = new B[Int] with C
  summonInline[Reader[bc.type, Int]] // (I) Works
  summonOne[Reader[bc.type, Int]] // (II) Errors

Looks like the only difference between the working call (I) and the erroring call (II) is that the failing call has a slightly different Context, the type argument is unchanged after the inlining of the enclosing method. In the failing call (II) the compiler is able to correctly identify the implicit method, however a later TypeComparer.testSubType check fails.

This is where it gets difficult, in both calls the TypeComparer tries to check the subtyping of Reader[A{type Q = X}, X] <:< Reader[(bc : B[Int] & C), Int], later checking B[Int] & C <:< A{type Q = X}, later checking B[Int].Q <:< X.

I have no idea why the things differ here. In the failing check this causes the X type variable to "become" B[Int]#F[B[Int]#T], later failing X <: Int check. Type comparer is not an area of the compiler I usually touch, so I have no idea why the B[Int].Q denotations differ in those 2 calls and I am a bit stuck (any help/ideas would be appreciated).

jchyb commented 1 month ago

Hi @noti0na1, as mentioned above, I ran into some problems while investigating this issue relating to the type comparer, which I do not fully understand. While reviewing the current cycle, @Gedochao recommended you as a TypeComparer expert. Any help/mentoring/hints would be appreciated (if you have time of course!, in my opinion this seems like a lower priority issue, but an annoying one nonetheless). Later this week I'll try to prepare a branch with type comparer printouts to show exactly what I mean in the message above, and so that you have a potentially easier time helping me dig there while ignoring the metaprogramming/implicit stuff. Even if you end up not being able to help/mentor, a branch like this will still be useful for the future me eventually, so do not feel pressured!

noti0na1 commented 1 month ago

I took a quick look, and it appears that the two summonInline calls are completed during different phases:

I tried modifying the evCtx within searchImplicitOrError to ctx.fresh.setTyper(evTyper).setPhase(ctx.base.typerPhase) (which is not the correct fix!), and with this change, the tests in this issue pass.

I know that the TypeComparer behaves differently before and after the typer phase, but I haven't look into details why the type variable can't be instantiated in the second case.

EugeneFlesselle commented 1 month ago

Further minimized to:

import scala.compiletime.*

trait Reader[-In, Out]

trait A:
  type F
  type Q = F

given [X]: Reader[A { type Q = X }, X] with {}

def main =
//  type BC = A { type F = Int } & A // ok
  type BC = A & A { type F = Int } // fail, also ok when manually de-aliased

  inline def summonOne: Unit = summonInline[Reader[BC, Int]]

  summonInline[Reader[BC, Int]] // ok
  summonOne // error

It is from hasMatchingMember that we get results depending on the current phase.

jchyb commented 1 week ago

@noti0na1 @EugeneFlesselle Thank you for all of the help!

The different behavior is indeed caused by the current phase. With that knowledge I tried digging a little further, and where it starts to differ is where it tries to return the denotation for A#Q. In typer it returns <SingleDenotation of type TypeBounds(TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Nothing),TypeRef(ThisType(TypeRef(NoPrefix,module class scala)),class Any))>, in inlining it returns <SingleDenotation of type TypeAlias(TypeRef(TypeRef(ThisType(TypeRef(NoPrefix,module class <empty>)),trait A),type F))>. It makes sense that the denotations are different for different phases, and I don't think I can change anything there, so I'll try to look at this from another angle.

EugeneFlesselle commented 6 days ago

We had also noticed the issue is avoided by disabling the optimizations in def compareRefined that try to avoid going through def compareRefinedSlow, which might be useful to look into as well @jchyb