Open kavedaa opened 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
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
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
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
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).
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!
I took a quick look, and it appears that the two summonInline
calls are completed during different phases:
summonInline[Reader[bc.type, Int]]
is inlined during typer, summonInline
inside summonOne
is inlined during inlining.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.
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.
@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.
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
Compiler version
3.2.2, 3.3.0-RC3
Minimized code
Output
Expectation
Successful compilation