diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTag.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTag.scala index 1e64aed2..96b7b8df 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTag.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTag.scala @@ -102,18 +102,68 @@ abstract class LightTypeTag private[reflect] ( * F[_, _, _].combine(A, B) = F[A, B, _] * }}} */ - def combine(args: LightTypeTag*): LightTypeTag = { - val argRefs = args.map(_.ref) - val appliedBases = basesdb ++ basesdb.iterator.collect { // do not remove the unapplied base lambdas after combination (required for inferredLambdaParents in isChild) - case (self: LightTypeTagRef.Lambda, parents) => - self.combine(argRefs) -> parents.map { - case l: LightTypeTagRef.Lambda => - l.combine(argRefs) - case nonLambdaParent => - val context = self.input.zip(argRefs.collect { case a: AbstractReference => a }).toMap - new RuntimeAPI.Rewriter(context).replaceRefs(nonLambdaParent) + private[this] def rewriteBases( + args: Seq[Option[AbstractReference]], + combineRootLambda: LightTypeTagRef.Lambda => AbstractReference + ): Map[AbstractReference, Set[AbstractReference]] = { + ref match { + case currentLambda: LightTypeTagRef.Lambda => + def reachableLambdaBases(todo: List[LightTypeTagRef.Lambda], seen: Set[LightTypeTagRef.Lambda]): Set[LightTypeTagRef.Lambda] = { + todo match { + case head :: tail if seen(head) => + reachableLambdaBases(tail, seen) + case head :: tail => + val next = basesdb.get(head).iterator.flatMap(_.iterator).collect { + case l: LightTypeTagRef.Lambda if l.input == currentLambda.input => + l + }.toList + reachableLambdaBases(next ::: tail, seen + head) + case Nil => + seen + } } + + val inheritedLambdas = reachableLambdaBases(currentLambda :: Nil, Set.empty) + val context = currentLambda.input.zip(args).collect { + case (param, Some(arg)) => + param -> arg + }.toMap + val rewriter = new RuntimeAPI.Rewriter(context) + + basesdb ++ basesdb.iterator.collect { + case (self: LightTypeTagRef.Lambda, parents) => + val rewrittenSelf = + if (inheritedLambdas(self)) { + combineRootLambda(self) + } else { + rewriter.replaceRefs(self) + } + + val rewrittenParents = parents.map { + case l: LightTypeTagRef.Lambda if inheritedLambdas(l) => + combineRootLambda(l) + case other => + rewriter.replaceRefs(other) + } + + if (rewrittenSelf != self || rewrittenParents != parents) { + Some(rewrittenSelf -> rewrittenParents) + } else { + None + } + }.flatten + + case _ => + basesdb } + } + + def combine(args: LightTypeTag*): LightTypeTag = { + val argRefs = args.map(_.ref match { case reference: AbstractReference => reference }) + val appliedBases = rewriteBases( + args = argRefs.map(Some(_)), + combineRootLambda = _.combine(argRefs) + ) def mergedBasesDB = LightTypeTag.mergeIDBs(appliedBases, args.iterator.map(_.basesdb)) @@ -132,17 +182,11 @@ abstract class LightTypeTag private[reflect] ( * }}} */ def combineNonPos(args: Option[LightTypeTag]*): LightTypeTag = { - val argRefs = args.map(_.map(_.ref)) - val appliedBases = basesdb ++ basesdb.iterator.collect { // do not remove the unapplied base lambdas after combination (required for inferredLambdaParents in isChild) - case (self: LightTypeTagRef.Lambda, parents) => - self.combineNonPos(argRefs) -> parents.map { - case l: LightTypeTagRef.Lambda => - l.combineNonPos(argRefs) - case nonLambdaParent => - val context = self.input.zip(argRefs.flatten.collect { case a: AbstractReference => a }).toMap - new RuntimeAPI.Rewriter(context).replaceRefs(nonLambdaParent) - } - } + val argRefs = args.map(_.map(_.ref match { case reference: AbstractReference => reference })) + val appliedBases = rewriteBases( + args = argRefs, + combineRootLambda = _.combineNonPos(argRefs) + ) def mergedBasesDB = LightTypeTag.mergeIDBs(appliedBases, args.iterator.map(_.map(_.basesdb).getOrElse(Map.empty))) diff --git a/izumi-reflect/izumi-reflect/src/test/scala-2/izumi/reflect/test/TagTest.scala b/izumi-reflect/izumi-reflect/src/test/scala-2/izumi/reflect/test/TagTest.scala index a6064929..3e550ca0 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala-2/izumi/reflect/test/TagTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala-2/izumi/reflect/test/TagTest.scala @@ -95,6 +95,30 @@ class TagTest extends SharedTagTest { assert(t[TraitK30, Trait30].tag == Tag[TraitK30[Trait30]].tag) } + "regression test for https://github.com/zio/izumi-reflect/issues/571: RuntimeAPI.unpack handles abstract type bounds with intersections" in { + trait Scope { + trait X + + type SomeType[S] <: S with X + + trait RootTrait[P] + trait TestTrait[F[_]] extends RootTrait[SomeType[String]] + } + object Scope extends Scope + import Scope._ + + def unpackTag[F[_]: TagK] = { + val ref = implicitly[Tag[TestTrait[F]]].tag.ref match { + case reference: LightTypeTagRef.AbstractReference => reference + } + RuntimeAPI.unpack(ref) + } + + type Id[A] = A + + assert(unpackTag[Id].nonEmpty) + } + "example in 3.0.9 release notes works" in { def printColType[F[+x] <: Iterable[x]: Tag.auto.T]: LightTypeTag = { Tag[F[Int]].tag 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 30dfe7be..8cb23c58 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 @@ -71,45 +71,6 @@ abstract class SharedLightTypeTagProgressionTest extends TagAssertions with TagP } } - "progression test: combined intersection lambda tags still contain some junk bases (coming from the unsound same-arity assumption in LightTypeTag#combine)" in { - val tCtor = `LTT[_,_]`[T3] - val combined = tCtor.combine(LTT[Int], LTT[Boolean]) - val debugCombined = combined.debug("combined") - - val alias = LTT[T3[Int, Boolean]] - val direct = LTT[W1 with W4[Boolean] with W5[Int]] - - broken { - assert(!debugCombined.contains("W4[=scala.Int]")) - } - broken { - assert(!debugCombined.contains("W3[=scala.Int]")) - } - - broken { - assertDebugSame(combined, alias) - } - broken { - assertDebugSame(combined, direct) - } - } - - "progression test: combined lambda tags still contain some junk bases (coming from the unsound same-arity assumption in LightTypeTag#combine)" in { - val curriedApplied = `LTT[_,_]`[Right].combine(LTT[Throwable]).combine(LTT[Unit]) - val debug1 = curriedApplied.debug() - - assertSame(curriedApplied, LTT[Right[Throwable, Unit]]) - - assert(debug1.contains(": scala.util.Right[+java.lang.Throwable,+scala.Unit]")) - assert(debug1.contains("- scala.util.Right[+java.lang.Throwable,+scala.Unit]")) - assert(debug1.contains("* scala.Product")) - assert(debug1.contains("- λ %1 → scala.util.Right[+java.lang.Throwable,+1]")) - assert(debug1.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) - broken { - assert(!debug1.contains("λ %1 → scala.util.Right[+scala.Unit,+1]")) - } - } - "progression test: Dotty fails to `support methods with type parameters in structural refinements`" in { trait X { def x[A](a: A): A } diff --git a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagTest.scala b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagTest.scala index 73995434..23872f56 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/SharedLightTypeTagTest.scala @@ -663,8 +663,12 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(!debugCombined.contains("W4[=B]")) assert(!debugCombined.contains("W3[=B]")) assert(!debugCombined.contains("W5[=A]")) + assert(!debugCombined.contains("W4[=scala.Int]")) + assert(!debugCombined.contains("W3[=scala.Int]")) assert(debugCombined.contains("W5[=scala.Int]")) + assertSame(combined, alias) + assertSame(combined, direct) assertDebugSame(alias, direct) } @@ -725,15 +729,20 @@ abstract class SharedLightTypeTagTest extends TagAssertions { val oneArgApplied = `LTT[_,_]`[Right].combine(LTT[Throwable]).combine(LTT[Unit]) val debug5 = oneArgApplied.debug() + assertSame(oneArgApplied, LTT[Right[Throwable, Unit]]) + assert(!debug5.contains("package::Right")) assert(!debug5.contains("")) assert(!debug5.contains("")) assert(!debug5.contains("scala.package.A")) assert(!debug5.contains("scala.package.B")) assert(!debug5.contains("+scala.Nothing")) + assert(debug5.contains(": scala.util.Right[+java.lang.Throwable,+scala.Unit]")) + assert(debug5.contains("- scala.util.Right[+java.lang.Throwable,+scala.Unit]")) assert(debug5.contains("* scala.Product")) assert(debug5.contains("- λ %1 → scala.util.Right[+java.lang.Throwable,+1]")) assert(debug5.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) + assert(!debug5.contains("λ %1 → scala.util.Right[+scala.Unit,+1]")) } "No degenerate lambdas (regression test https://github.com/zio/izumi-reflect/issues/345)" in {