Skip to content
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

Scala 3: fix failures to combine higher-kinded type lambdas where type parameters of a higher-kinded type parameter were not extracted correctly #365

Merged
merged 1 commit into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package izumi.reflect.dottyreflection
import izumi.reflect.macrortti.{LightTypeTagInheritance, LightTypeTagRef}
import izumi.reflect.macrortti.LightTypeTagRef.*

import scala.annotation.tailrec
import scala.annotation.{tailrec, targetName}
import scala.quoted.Type
import scala.reflect.Selectable.reflectiveSelectable

Expand Down Expand Up @@ -32,12 +32,27 @@ abstract class Inspector(protected val shift: Int) extends InspectorBase {
case Nil =>
makeNameReferenceFromType(a.tycon)
case _ =>
// https://github.com/lampepfl/dotty/issues/8520
val params = a.tycon.typeSymbol.typeMembers
val zargs = a.args.zip(params)
val args = zargs.map(next().inspectTypeParam)
val nameref = makeNameReferenceFromType(a.tycon)
FullReference(nameref.ref.name, args, prefix = nameref.prefix)
val symbolVariances = a.tycon.typeSymbol.typeMembers.map(extractVariance)
val variances = if (symbolVariances.sizeCompare(a.args) < 0) {
a.tycon match {
case typeParamRef: ParamRef =>
typeParamRef._underlying match {
case TypeBounds(_, hi) =>
hi._declaredVariancesIfHKTypeLambda.fold(Nil)(_.map(extractVariance))
case _ =>
Nil
}
case _ =>
Nil
}
} else {
symbolVariances
}
val nameRef = makeNameReferenceFromType(a.tycon)
val args = a
.args.iterator.zipAll(variances, null.asInstanceOf[TypeRepr], Variance.Invariant).takeWhile(_._1 != null)
.map(next().inspectTypeParam).toList
FullReference(ref = nameRef.ref.name, parameters = args, prefix = nameRef.prefix)
}

case l: TypeLambda =>
Expand Down Expand Up @@ -143,10 +158,7 @@ abstract class Inspector(protected val shift: Int) extends InspectorBase {
}
}

private def inspectTypeParam(tpe: TypeRepr, td: Symbol): TypeParam = {
val variance = extractVariance(td)
// TypeParam(inspectTypeRepr(tpe), variance)

private def inspectTypeParam(tpe: TypeRepr, variance: Variance): TypeParam = {
tpe match {
case t: TypeBounds =>
TypeParam(inspectBounds(None, t, isParam = true), variance)
Expand All @@ -157,9 +169,14 @@ abstract class Inspector(protected val shift: Int) extends InspectorBase {
}

private def extractVariance(t: Symbol): Variance = {
if (t.flags.is(Flags.Covariant)) {
extractVariance(t.flags)
}

@targetName("extractVarianceFlags")
private def extractVariance(flags: Flags): Variance = {
if (flags.is(Flags.Covariant)) {
Variance.Covariant
} else if (t.flags.is(Flags.Contravariant)) {
} else if (flags.is(Flags.Contravariant)) {
Variance.Contravariant
} else {
Variance.Invariant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,27 @@ private[dottyreflection] trait ReflectionUtil { this: InspectorBase =>

extension (typeRef: TypeRef | ParamRef) {
protected def _underlying: TypeRepr = {
// val underlying = typeRef
// .getClass.getMethods.collect { case m if m.getName == "underlying" => m }.head.invoke(
// typeRef,
// qctx.getClass.getMethods.collect { case m if m.getName == "ctx" => m }.head.invoke(qctx)
// )
// underlying.asInstanceOf[TypeRepr]

// This works as a substitution for `TypeRef#underlying` call,
// but I'm not sure if it's a reliable substitution.
typeRef.typeSymbol.owner._typeRef.memberType(typeRef.typeSymbol)

// typeRef.typeSymbol.owner._typeRef.memberType(typeRef.typeSymbol)

// No, It's not a reliable substitution. When used on a TypeParamRef it returns Any instead of the underlying TypeBounds
// https://github.com/lampepfl/dotty/issues/15799

val underlying = typeRef

Choose a reason for hiding this comment

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

@neko-kai Another way to experiment using the compiler internals could be

import scala.reflect.Selectable.reflectiveSelectable
type Context
type InternalQuotes = {
  def ctx: Context
}
type InternalTypeRef = {
  def underlying(using Context): TypeRepr
}
given Context = qctx.asInstanceOf[InternalQuotes].ctx
typeRef.asInstanceOf[InternalTypeRef].underlying

Copy link
Member Author

Choose a reason for hiding this comment

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

@nicolasstucki I'll keep this in mind. Thanks! 👍

.getClass.getMethods.collect { case m if m.getName == "underlying" => m }.head.invoke(
typeRef,
qctx.getClass.getMethods.collect { case m if m.getName == "ctx" => m }.head.invoke(qctx)
)
underlying.asInstanceOf[TypeRepr]
}
}

extension (typeRepr: TypeRepr) {
protected def _declaredVariancesIfHKTypeLambda: Option[List[Flags]] = {
val maybeMethod = typeRepr.getClass.getMethods.collectFirst { case m if m.getName == "declaredVariances" => m }
maybeMethod.map(_.invoke(typeRepr).asInstanceOf[List[Flags]])
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,36 +228,6 @@ abstract class SharedLightTypeTagProgressionTest extends TagAssertions with TagP
}
}

"progression test: a portion of `support subtyping of parents parameterized with type lambdas in combined tags` fails on Dotty" in {
val childBase = `LTT[_[_,_]]`[RoleChild]
val childArg = `LTT[_,_]`[Either]
val combinedTag = childBase.combine(childArg)
val parentTag = LTT[RoleParent[Either[Throwable, *]]]
val childTag = LTT[RoleChild[Either]]

assertChild(combinedTag, childTag)
assertSame(combinedTag, childTag)

doesntWorkYetOnDotty {
assertChild(combinedTag, parentTag)
assertNotChild(parentTag, combinedTag)
}
}

"progression test: a portion of `support subtyping of parents parameterized with type lambdas in combined tags with multiple parameters` fails on Dotty" in {
val childBase = `LTT[_[+_,+_],_,_]`[RoleChild2]
val childArgs = Seq(`LTT[_,_]`[Either], LTT[Int], LTT[String])
val combinedTag = childBase.combine(childArgs: _*)
val expectedTag = LTT[RoleParent[Either[Throwable, *]]]
val noncombinedTag = LTT[RoleChild2[Either, Int, String]]

assertSame(combinedTag, noncombinedTag)
assertChild(noncombinedTag, expectedTag)
doesntWorkYetOnDotty {
assertChild(combinedTag, expectedTag)
}
}

"progression test: in `support literal types` literal encoding in Dotty version doesn't match Scala 2" in {
val tag = literalLtt("str")
assertRepr(tag, "\"str\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,23 +197,6 @@ abstract class SharedTagProgressionTest extends AnyWordSpec with TagAssertions w
}
}

"progression test: Dotty fails combine higher-kinded type lambdas without losing ignored type arguments" in {
val tag = `LTT[_[+_,+_]]`[({ type l[F[+_, +_]] = BlockingIO3[λ[(`-R`, `+E`, `+A`) => F[E, A]]] })#l]
val res = tag.combine(`LTT[_,_]`[IO])
doesntWorkYetOnDotty {
assertSame(res, LTT[BlockingIO[IO]])
}
}

"progression test: Dotty fails resolve a higher-kinded type inside a named type lambda with ignored type arguments" in {
def mk[F[+_, +_]: TagKK] = Tag[BlockingIO3[F2To3[F, *, *, *]]]
val tag = mk[IO]

doesntWorkYetOnDotty {
assert(tag.tag == Tag[BlockingIO[IO]].tag)
}
}

"Progression test: Scala 2 fails to Handle Tags outside of a predefined set (Somehow raw Tag.auto.T works on Scala 2, but not when defined as an alias)" in {
type TagX[F[_, _, _[_[_], _], _[_], _]] = Tag.auto.T[F]
// type TagX[K[_, _, _[_[_], _], _[_], _]] = HKTag[{ type Arg[T1, T2, T3[_[_], _], T4[_], T5] = K[T1, T2, T3, T4, T5] }]
Expand Down Expand Up @@ -246,30 +229,6 @@ abstract class SharedTagProgressionTest extends AnyWordSpec with TagAssertions w
}
}

"progression test: Dotty fails to resolve TagKK from an odd higher-kinded Tag with swapped & ignored parameters (low-level)" in {
type Lt[F[_, _, _], _1, _2, _3] = F[_2, _3, _1]

val ctorTag: LightTypeTag = implicitly[Tag.auto.T[Lt]].tag
val eitherRSwapTag = LTagK3[EitherRSwap].tag
val throwableTag = LTag[Throwable].tag

val combinedTag = HKTag
.appliedTagNonPosAux(
classOf[Any],
ctor = ctorTag,
args = List(
Some(eitherRSwapTag),
Some(throwableTag),
None,
None
)
).tag
val expectedTag = TagKK[Lt[EitherRSwap, Throwable, *, *]].tag
doesntWorkYetOnDotty {
assertSame(combinedTag, expectedTag)
}
}

"progression test: Dotty fails to can resolve parameters in structural types" in {
def t[X: Tag]: Tag[{ type T = X }] = Tag[{ type T = X }]

Expand All @@ -288,21 +247,6 @@ abstract class SharedTagProgressionTest extends AnyWordSpec with TagAssertions w
}
}

"progression test: Dotty fails to correctly resolve a higher-kinded nested type inside a named swap type lambda" in {
def mk[F[+_, +_]: TagKK] = Tag[BIOService[SwapF2[F, *, *]]]
val tag = mk[Either]

doesntWorkYetOnDotty {
assert(tag.tag == Tag[BIOService[SwapF2[Either, *, *]]].tag)
}
doesntWorkYetOnDotty {
assert(tag.tag == Tag[BIOService[Swap]].tag)
}
doesntWorkYetOnDotty {
assert(tag.tag == Tag[BIOService[λ[(E, A) => Either[A, E]]]].tag)
}
}

"progression test: Dotty fails to correctly resolve a higher-kinded nested type inside an anonymous swap type lambda" in {
def mk[F[+_, +_]: TagKK] = Tag[BIOService[λ[(E, A) => F[A, E]]]]
val tag = mk[Either]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package izumi.reflect.test

import izumi.reflect.{Tag, TagK3, _}
import izumi.reflect.macrortti.LightTypeTag.ParsedLightTypeTag210
import izumi.reflect.macrortti.{LTT, LTag, LTagK3, LightTypeTag}
import izumi.reflect.macrortti._
import izumi.reflect.test.ID._
import izumi.reflect.test.TestModel.{ApplePaymentProvider, Const, H1, IO, IdAnnotation, ThisPrefixTest, VarArgsAnyVal}
import izumi.reflect.test.TestModel._
import izumi.reflect.thirdparty.internal.boopickle.PickleImpl
import izumi.reflect._
import org.scalatest.Assertions
import org.scalatest.exceptions.TestFailedException
import org.scalatest.wordspec.AnyWordSpec
Expand Down Expand Up @@ -684,6 +684,76 @@ abstract class SharedTagTest extends AnyWordSpec with XY[String] with TagAsserti
assert(zy.tagA.isSuccess)
}

"combine higher-kinded type lambdas without losing ignored type arguments" in {
val tag = `LTT[_[+_,+_]]`[({ type l[F[+_, +_]] = BlockingIO3[λ[(`-R`, `+E`, `+A`) => F[E, A]]] })#l]
val res = tag.combine(`LTT[_,_]`[IO])
assertSameStrict(res, LTT[BlockingIO[IO]])
}

"resolve a higher-kinded type inside a named type lambda with ignored type arguments" in {
def mk[F[+_, +_]: TagKK] = Tag[BlockingIO3[F2To3[F, *, *, *]]]
val tag = mk[IO]
assertSameStrict(tag.tag, Tag[BlockingIO[IO]].tag)
}

"resolve TagKK from an odd higher-kinded Tag with swapped & ignored parameters (low-level)" in {
type Lt[F[_, _, _], _1, _2, _3] = F[_2, _3, _1]

val ctorTag: LightTypeTag = implicitly[Tag.auto.T[Lt]].tag
val eitherRSwapTag = LTagK3[EitherRSwap].tag
val throwableTag = LTag[Throwable].tag

val combinedTag = HKTag
.appliedTagNonPosAux(
classOf[Any],
ctor = ctorTag,
args = List(
Some(eitherRSwapTag),
Some(throwableTag),
None,
None
)
).tag
val expectedTag = TagKK[Lt[EitherRSwap, Throwable, *, *]].tag
assertSameStrict(combinedTag, expectedTag)
}

"correctly resolve a higher-kinded nested type inside a named swap type lambda" in {
def mk[F[+_, +_]: TagKK] = Tag[BIOService[SwapF2[F, *, *]]]

val tag = mk[Either]

assertSameStrict(tag.tag, Tag[BIOService[SwapF2[Either, *, *]]].tag)
assertSameStrict(tag.tag, Tag[BIOService[Swap]].tag)
assertSameStrict(tag.tag, Tag[BIOService[λ[(E, A) => Either[A, E]]]].tag)
}

"support subtyping of parents parameterized with type lambdas in combined tags" in {
val childBase = `LTT[_[_,_]]`[RoleChild]
val childArg = `LTT[_,_]`[Either]
val combinedTag = childBase.combine(childArg)
val parentTag = LTT[RoleParent[Either[Throwable, *]]]
val childTag = LTT[RoleChild[Either]]

assertChild(combinedTag, childTag)
assertSame(combinedTag, childTag)

assertChild(combinedTag, parentTag)
assertNotChild(parentTag, combinedTag)
}

"support subtyping of parents parameterized with type lambdas in combined tags with multiple parameters" in {
val childBase = `LTT[_[+_,+_],_,_]`[RoleChild2]
val childArgs = Seq(`LTT[_,_]`[Either], LTT[Int], LTT[String])
val combinedTag = childBase.combine(childArgs: _*)
val expectedTag = LTT[RoleParent[Either[Throwable, *]]]
val noncombinedTag = LTT[RoleChild2[Either, Int, String]]

assertSame(combinedTag, noncombinedTag)
assertChild(noncombinedTag, expectedTag)
assertChild(combinedTag, expectedTag)
}

}

}
Expand Down