diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/Inspector.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/Inspector.scala index ac78b7df..5e407920 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/Inspector.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/Inspector.scala @@ -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 @@ -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 => @@ -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) @@ -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 diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/ReflectionUtil.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/ReflectionUtil.scala index 368c455e..2d1bc3e1 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/ReflectionUtil.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/ReflectionUtil.scala @@ -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 + .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]]) } } diff --git a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagProgressionTest.scala b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagProgressionTest.scala index 056779a6..8e256918 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagProgressionTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagProgressionTest.scala @@ -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\"") diff --git a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagProgressionTest.scala b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagProgressionTest.scala index eff45fb7..b70c9050 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagProgressionTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagProgressionTest.scala @@ -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] }] @@ -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 }] @@ -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] diff --git a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagTest.scala b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagTest.scala index ad127010..d051d9a5 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedTagTest.scala @@ -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 @@ -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) + } + } }