diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagRef.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagRef.scala index 3cd68f4f..3c9d4bf8 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagRef.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagRef.scala @@ -57,6 +57,7 @@ sealed trait LightTypeTagRef extends LTTSyntax with Serializable { } object LightTypeTagRef extends LTTOrdering { + private[reflect] final val lambdaFakeParamDepth: Int = -2 import LTTRenderables.Short._ // import LTTRenderables.Long._ @@ -96,8 +97,75 @@ object LightTypeTagRef extends LTTOrdering { unusedParamsSize < paramRefs.size } - lazy val normalizedParams: List[NameReference] = makeFakeParams.map(_._2) - lazy val normalizedOutput: AbstractReference = RuntimeAPI.applyLambda(this, makeFakeParams) + // New normalization logic + private def normalizeLambda( + ref: AbstractReference, + currentDepth: Int + ): AbstractReference = ref match { + + case NameReference(SymName.LambdaParamName(i, _, a), b, p) => + NameReference( + SymName.LambdaParamName(i, currentDepth, a), + b, + p + ) + + case Lambda(in, out) => + Lambda( + in, + normalizeLambda(out, currentDepth + 1) + ) + + case FullReference(sym, params, prefix) => + FullReference( + sym, + params.map(p => p.copy(ref = normalizeLambda(p.ref, currentDepth))), + prefix.map(p => normalizeLambda(p, currentDepth).asInstanceOf[AppliedReference]) + ) + + case IntersectionReference(refs) => + IntersectionReference( + refs.map(r => + normalizeLambda(r, currentDepth) + .asInstanceOf[AppliedReferenceExceptIntersection] + ) + ) + + case UnionReference(refs) => + UnionReference( + refs.map(r => + normalizeLambda(r, currentDepth) + .asInstanceOf[AppliedReferenceExceptUnion] + ) + ) + + case Refinement(ref, decls) => + Refinement( + normalizeLambda(ref, currentDepth).asInstanceOf[AppliedReference], + decls.map { + case RefinementDecl.Signature(n, in, out) => + RefinementDecl.Signature( + n, + in.map(i => normalizeLambda(i, currentDepth).asInstanceOf[AppliedReference]), + normalizeLambda(out, currentDepth).asInstanceOf[AppliedReference] + ) + case RefinementDecl.TypeMember(n, r) => + RefinementDecl.TypeMember(n, normalizeLambda(r, currentDepth)) + } + ) + + case other => + other + } + + lazy val normalizedOutput: AbstractReference = { + val applied = RuntimeAPI.applyLambda(this, makeFakeParams) + normalizeLambda(applied, currentDepth = 0) + } + + @deprecated("Binary compatibility shim. Do not use.", "2.3.0") + def normalizedParams: List[AbstractReference] = + Nil override def equals(obj: Any): Boolean = { obj match { @@ -113,12 +181,13 @@ object LightTypeTagRef extends LTTOrdering { private[this] def makeFakeParams: List[(LambdaParamName, NameReference)] = { input.zipWithIndex.map { case (p, idx) => - p -> NameReference(SymName.LambdaParamName(idx, lambdaFakeParamDepth, inputSize)) // s"!FAKE_$idx" + p -> NameReference( + SymName.LambdaParamName(idx, lambdaFakeParamDepth, inputSize) + ) } } - } - private[reflect] final val lambdaFakeParamDepth: Int = -2 // depth is always positive, unless fake + } sealed trait AppliedReference extends AbstractReference 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..e16b8de8 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 @@ -736,6 +736,7 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(debug5.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) } + "No degenerate lambdas (regression test https://github.com/zio/izumi-reflect/issues/345)" in { val fullDb = LTT[List[Int]].basesdb @@ -1324,6 +1325,94 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assertChildStrict(child, parent) } + "normalize lambda parameter depths (regression #379)" in { + type Bin[A, B] = Tuple2[A, B] + + val tag = `LTT[_,_]`[Bin] + + val lambda = tag.ref match { + case l: LightTypeTagRef.Lambda => l + case other => fail(s"Expected lambda, got: $other") + } + + val preNormDepths = lambda.input.map(_.depth) + assert(preNormDepths.nonEmpty) + + val normalized = lambda.normalizedOutput + + def collectDepths(ref: izumi.reflect.macrortti.LightTypeTagRef.AbstractReference): List[Int] = ref match { + case izumi.reflect.macrortti.LightTypeTagRef.NameReference(izumi.reflect.macrortti.LightTypeTagRef.SymName.LambdaParamName(_, depth: Int, _), _, _) => + depth :: Nil + + case izumi.reflect.macrortti.LightTypeTagRef.Lambda(_, out) => + collectDepths(out) + + case izumi.reflect.macrortti.LightTypeTagRef.FullReference(_, params, prefix) => + params.flatMap(p => collectDepths(p.ref)) ++ prefix.toList.flatMap(collectDepths) + + case izumi.reflect.macrortti.LightTypeTagRef.IntersectionReference(refs) => + refs.toList.flatMap(r => collectDepths(r)) + + case izumi.reflect.macrortti.LightTypeTagRef.UnionReference(refs) => + refs.toList.flatMap(r => collectDepths(r)) + + case izumi.reflect.macrortti.LightTypeTagRef.Refinement(ref0, decls) => + collectDepths(ref0) ++ decls.flatMap { + case izumi.reflect.macrortti.LightTypeTagRef.RefinementDecl.Signature(_, in, out) => + in.flatMap(collectDepths) ++ collectDepths(out) + case izumi.reflect.macrortti.LightTypeTagRef.RefinementDecl.TypeMember(_, r) => + collectDepths(r) + } + + case _ => + Nil + } + + val postNormDepths = collectDepths(normalized) + assert(!postNormDepths.exists(_ < 0)) + } + + "normalized lambda string representation does not contain FAKE_ parameters (regression #379)" in { + // Test simple type lambda: λ %0,%1 → Tuple2[+0,+1] + type Bin[A, B] = Tuple2[A, B] + val binTag = `LTT[_,_]`[Bin] + val binRepr = binTag.repr + + assert(!binRepr.contains("FAKE_"), s"Lambda repr should not contain FAKE_ parameters: $binRepr") + assert(binRepr.contains("λ %0,"), s"Lambda repr should contain λ %0,: $binRepr") + assert(binRepr.contains("Tuple2["), s"Lambda repr should contain Tuple2[: $binRepr") + + // Test nested type lambda matching issue #379 pattern: + // outer lambda takes F[_], inner lambda applies F with parameters from inner scope + // Expected form: λ %0 → FM2[+0[=Either[+W1,+W2]]] + type NestedLambda[F[_]] = FM2[F[Either[W1, W2]]] + val nestedTag = `LTT[_[_]]`[({ type l[F[_]] = NestedLambda[F] })#l] + val nestedRepr = nestedTag.repr + + assert(!nestedRepr.contains("FAKE_"), s"Nested lambda repr should not contain FAKE_ parameters: $nestedRepr") + assert(nestedRepr.contains("λ %0"), s"Nested lambda repr should contain λ %0: $nestedRepr") + assert(nestedRepr.contains("FM2["), s"Nested lambda repr should contain FM2[: $nestedRepr") + + // Verify the nested lambda structure does not have corrupted parameter references + val nestedStr = nestedTag.ref.toString + assert(!nestedStr.contains("FAKE_"), s"Nested lambda toString should not contain FAKE_ parameters: $nestedStr") + + // Regression #379 core requirement: verify outer-scope references use proper depth markers + // Using a concrete type that includes RoleChild as a type argument exposes the full + // nested lambda structure in debug output, where outer-scope refs appear as "1:0". + // RoleChild[F[_, _]] extends RoleParent[F[Throwable, *]] creates an inner lambda + // λ %1:0 → 0[=Throwable,=1:0] where "1:0" references the outer lambda's F parameter. + val roleChildDebug = LTT[Either[RoleChild[IO], Unit]].debug() + + assert(!roleChildDebug.contains("FAKE_"), s"RoleChild debug should not contain FAKE_ parameters: $roleChildDebug") + + // The key assertion: the nested lambda inside RoleParent must render outer-scope + // references with depth markers. The pattern "1:0" appears in: + // λ %1:0 → 0[=java.lang.Throwable,=1:0] + // where "1:0" is the reference to F (param index 0) from the outer lambda (depth 1). + assert(roleChildDebug.contains("1:0"), + s"Nested lambda should contain depth-prefixed param refs '1:0' for outer-scope references: $roleChildDebug") + } } }