From 9b5c600a04f375f147cb4320f50bbb53d02c05be Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 15:47:18 +0530 Subject: [PATCH 01/10] fix: normalize lambda parameters by nested scope depth Signed-off-by: Rishi Jat --- .../reflect/macrortti/LightTypeTagRef.scala | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) 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 c2eff2f0..4524757a 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 @@ -94,8 +94,28 @@ object LightTypeTagRef extends LTTOrdering { unusedParamsSize < paramRefs.size } - lazy val normalizedParams: List[NameReference] = makeFakeParams.map(_._2) - lazy val normalizedOutput: AbstractReference = RuntimeAPI.applyLambda(this, makeFakeParams) + // Normalization helper for lambda parameters and output + private def normalize(ref: AbstractReference, currentDepth: Int): AbstractReference = { + ref match { + case NameReference( + SymName.LambdaParamName(index, depth, arity), + boundaries, + prefix + ) => + NameReference( + SymName.LambdaParamName(index, depth - currentDepth, arity), + boundaries, + prefix + ) + case l @ Lambda(_, out) => + l.copy(output = normalize(out, currentDepth + 1)) + case other => + RuntimeAPI.mapReferences(other)(normalize(_, currentDepth)) + } + } + + lazy val normalizedOutput: AbstractReference = + normalize(output, currentDepth = 0) override def equals(obj: Any): Boolean = { obj match { @@ -108,16 +128,8 @@ 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" - } - } } - private[reflect] final val lambdaFakeParamDepth: Int = -2 // depth is always positive, unless fake - sealed trait AppliedReference extends AbstractReference sealed trait AppliedReferenceExceptIntersection extends AppliedReference From cfc883b59ef5fa1d8bc42adfb68d4cfda43e8233 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 16:20:19 +0530 Subject: [PATCH 02/10] fix ci Signed-off-by: Rishi Jat --- .../reflect/macrortti/LightTypeTagRef.scala | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) 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 4524757a..78dbbcd9 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 @@ -55,6 +55,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._ @@ -110,13 +111,52 @@ object LightTypeTagRef extends LTTOrdering { case l @ Lambda(_, out) => l.copy(output = normalize(out, currentDepth + 1)) case other => - RuntimeAPI.mapReferences(other)(normalize(_, currentDepth)) + other match { + case FullReference(sym, params, prefix) => + FullReference( + sym, + params.map(p => p.copy(ref = normalize(p.ref, currentDepth))), + prefix.map(p => normalize(p, currentDepth).asInstanceOf[AppliedReference]) + ) + + case IntersectionReference(refs) => + IntersectionReference( + refs.map(r => normalize(r, currentDepth).asInstanceOf[AppliedReferenceExceptIntersection]) + ) + + case UnionReference(refs) => + UnionReference( + refs.map(r => normalize(r, currentDepth).asInstanceOf[AppliedReferenceExceptUnion]) + ) + + case Refinement(ref, decls) => + Refinement( + normalize(ref, currentDepth).asInstanceOf[AppliedReference], + decls.map { + case RefinementDecl.Signature(n, in, out) => + RefinementDecl.Signature( + n, + in.map(i => normalize(i, currentDepth).asInstanceOf[AppliedReference]), + normalize(out, currentDepth).asInstanceOf[AppliedReference] + ) + case RefinementDecl.TypeMember(n, r) => + RefinementDecl.TypeMember(n, normalize(r, currentDepth)) + } + ) + + case _ => + other + } } } lazy val normalizedOutput: AbstractReference = normalize(output, 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 { case l: Lambda => From f85895c42e225f30650d3b024b44fed335446936 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 17:18:34 +0530 Subject: [PATCH 03/10] Normalize lambda parameter depths from innermost scope --- .../reflect/macrortti/LightTypeTagRef.scala | 129 +++++++++++------- 1 file changed, 77 insertions(+), 52 deletions(-) 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 78dbbcd9..e90da9cc 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 @@ -95,63 +95,88 @@ object LightTypeTagRef extends LTTOrdering { unusedParamsSize < paramRefs.size } - // Normalization helper for lambda parameters and output - private def normalize(ref: AbstractReference, currentDepth: Int): AbstractReference = { - ref match { - case NameReference( - SymName.LambdaParamName(index, depth, arity), - boundaries, - prefix - ) => - NameReference( - SymName.LambdaParamName(index, depth - currentDepth, arity), - boundaries, - prefix - ) - case l @ Lambda(_, out) => - l.copy(output = normalize(out, currentDepth + 1)) - case other => - other match { - case FullReference(sym, params, prefix) => - FullReference( - sym, - params.map(p => p.copy(ref = normalize(p.ref, currentDepth))), - prefix.map(p => normalize(p, currentDepth).asInstanceOf[AppliedReference]) - ) - - case IntersectionReference(refs) => - IntersectionReference( - refs.map(r => normalize(r, currentDepth).asInstanceOf[AppliedReferenceExceptIntersection]) - ) - - case UnionReference(refs) => - UnionReference( - refs.map(r => normalize(r, currentDepth).asInstanceOf[AppliedReferenceExceptUnion]) - ) + // Helper methods for normalization + private def maxLambdaDepth(ref: AbstractReference): Int = ref match { + case NameReference(SymName.LambdaParamName(_, depth, _), _, _) => + depth + + case Lambda(_, out) => + maxLambdaDepth(out) + + case FullReference(_, params, prefix) => + (params.map(p => maxLambdaDepth(p.ref)) ++ prefix.map(maxLambdaDepth)) + .foldLeft(0)(math.max) + + case IntersectionReference(refs) => + refs.map(maxLambdaDepth).foldLeft(0)(math.max) + + case UnionReference(refs) => + refs.map(maxLambdaDepth).foldLeft(0)(math.max) + + case Refinement(ref, decls) => + (maxLambdaDepth(ref) :: + decls.map { + case RefinementDecl.Signature(_, in, out) => + (in.map(maxLambdaDepth) :+ maxLambdaDepth(out)).max + case RefinementDecl.TypeMember(_, r) => + maxLambdaDepth(r) + }.toList).max + + case _ => + 0 + } - case Refinement(ref, decls) => - Refinement( - normalize(ref, currentDepth).asInstanceOf[AppliedReference], - decls.map { - case RefinementDecl.Signature(n, in, out) => - RefinementDecl.Signature( - n, - in.map(i => normalize(i, currentDepth).asInstanceOf[AppliedReference]), - normalize(out, currentDepth).asInstanceOf[AppliedReference] - ) - case RefinementDecl.TypeMember(n, r) => - RefinementDecl.TypeMember(n, normalize(r, currentDepth)) - } + private def renumber(ref: AbstractReference, maxDepth: Int): AbstractReference = ref match { + case NameReference(SymName.LambdaParamName(i, d, a), b, p) => + NameReference( + SymName.LambdaParamName(i, d - maxDepth, a), + b, + p + ) + + case Lambda(in, out) => + Lambda(in, renumber(out, maxDepth)) + + case FullReference(sym, params, prefix) => + FullReference( + sym, + params.map(p => p.copy(ref = renumber(p.ref, maxDepth))), + prefix.map(p => renumber(p, maxDepth).asInstanceOf[AppliedReference]) + ) + + case IntersectionReference(refs) => + IntersectionReference( + refs.map(r => renumber(r, maxDepth).asInstanceOf[AppliedReferenceExceptIntersection]) + ) + + case UnionReference(refs) => + UnionReference( + refs.map(r => renumber(r, maxDepth).asInstanceOf[AppliedReferenceExceptUnion]) + ) + + case Refinement(ref, decls) => + Refinement( + renumber(ref, maxDepth).asInstanceOf[AppliedReference], + decls.map { + case RefinementDecl.Signature(n, in, out) => + RefinementDecl.Signature( + n, + in.map(i => renumber(i, maxDepth).asInstanceOf[AppliedReference]), + renumber(out, maxDepth).asInstanceOf[AppliedReference] ) - - case _ => - other + case RefinementDecl.TypeMember(n, r) => + RefinementDecl.TypeMember(n, renumber(r, maxDepth)) } - } + ) + + case other => + other } - lazy val normalizedOutput: AbstractReference = - normalize(output, currentDepth = 0) + lazy val normalizedOutput: AbstractReference = { + val depth = maxLambdaDepth(output) + renumber(output, depth) + } @deprecated("Binary compatibility shim. Do not use.", "2.3.0") def normalizedParams: List[AbstractReference] = From 40103c8725cfd6c00d5459d3a30fd7829dd2e0a3 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 17:27:59 +0530 Subject: [PATCH 04/10] Fix lambda normalization by renumbering parameters from innermost scope Signed-off-by: Rishi Jat --- .../reflect/macrortti/LightTypeTagRef.scala | 73 +++++++------------ 1 file changed, 27 insertions(+), 46 deletions(-) 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 e90da9cc..75b03f6a 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 @@ -95,77 +95,60 @@ object LightTypeTagRef extends LTTOrdering { unusedParamsSize < paramRefs.size } - // Helper methods for normalization - private def maxLambdaDepth(ref: AbstractReference): Int = ref match { - case NameReference(SymName.LambdaParamName(_, depth, _), _, _) => - depth + // New normalization logic + private def normalizeLambda( + ref: AbstractReference, + currentDepth: Int + ): AbstractReference = ref match { - case Lambda(_, out) => - maxLambdaDepth(out) - - case FullReference(_, params, prefix) => - (params.map(p => maxLambdaDepth(p.ref)) ++ prefix.map(maxLambdaDepth)) - .foldLeft(0)(math.max) - - case IntersectionReference(refs) => - refs.map(maxLambdaDepth).foldLeft(0)(math.max) - - case UnionReference(refs) => - refs.map(maxLambdaDepth).foldLeft(0)(math.max) - - case Refinement(ref, decls) => - (maxLambdaDepth(ref) :: - decls.map { - case RefinementDecl.Signature(_, in, out) => - (in.map(maxLambdaDepth) :+ maxLambdaDepth(out)).max - case RefinementDecl.TypeMember(_, r) => - maxLambdaDepth(r) - }.toList).max - - case _ => - 0 - } - - private def renumber(ref: AbstractReference, maxDepth: Int): AbstractReference = ref match { - case NameReference(SymName.LambdaParamName(i, d, a), b, p) => + case NameReference(SymName.LambdaParamName(i, _, a), b, p) => NameReference( - SymName.LambdaParamName(i, d - maxDepth, a), + SymName.LambdaParamName(i, currentDepth, a), b, p ) case Lambda(in, out) => - Lambda(in, renumber(out, maxDepth)) + Lambda( + in, + normalizeLambda(out, currentDepth + 1) + ) case FullReference(sym, params, prefix) => FullReference( sym, - params.map(p => p.copy(ref = renumber(p.ref, maxDepth))), - prefix.map(p => renumber(p, maxDepth).asInstanceOf[AppliedReference]) + 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 => renumber(r, maxDepth).asInstanceOf[AppliedReferenceExceptIntersection]) + refs.map(r => + normalizeLambda(r, currentDepth) + .asInstanceOf[AppliedReferenceExceptIntersection] + ) ) case UnionReference(refs) => UnionReference( - refs.map(r => renumber(r, maxDepth).asInstanceOf[AppliedReferenceExceptUnion]) + refs.map(r => + normalizeLambda(r, currentDepth) + .asInstanceOf[AppliedReferenceExceptUnion] + ) ) case Refinement(ref, decls) => Refinement( - renumber(ref, maxDepth).asInstanceOf[AppliedReference], + normalizeLambda(ref, currentDepth).asInstanceOf[AppliedReference], decls.map { case RefinementDecl.Signature(n, in, out) => RefinementDecl.Signature( n, - in.map(i => renumber(i, maxDepth).asInstanceOf[AppliedReference]), - renumber(out, maxDepth).asInstanceOf[AppliedReference] + in.map(i => normalizeLambda(i, currentDepth).asInstanceOf[AppliedReference]), + normalizeLambda(out, currentDepth).asInstanceOf[AppliedReference] ) case RefinementDecl.TypeMember(n, r) => - RefinementDecl.TypeMember(n, renumber(r, maxDepth)) + RefinementDecl.TypeMember(n, normalizeLambda(r, currentDepth)) } ) @@ -173,10 +156,8 @@ object LightTypeTagRef extends LTTOrdering { other } - lazy val normalizedOutput: AbstractReference = { - val depth = maxLambdaDepth(output) - renumber(output, depth) - } + lazy val normalizedOutput: AbstractReference = + normalizeLambda(output, currentDepth = 0) @deprecated("Binary compatibility shim. Do not use.", "2.3.0") def normalizedParams: List[AbstractReference] = From 820423e15ff767193540efbc22eee331cdba6153 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 17:38:43 +0530 Subject: [PATCH 05/10] fix Signed-off-by: Rishi Jat --- .../izumi/reflect/macrortti/LightTypeTagRef.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 75b03f6a..28bbd86e 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 @@ -156,8 +156,10 @@ object LightTypeTagRef extends LTTOrdering { other } - lazy val normalizedOutput: AbstractReference = - normalizeLambda(output, currentDepth = 0) + 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] = @@ -174,6 +176,15 @@ object LightTypeTagRef extends LTTOrdering { } } + private[this] def makeFakeParams: List[(LambdaParamName, NameReference)] = { + input.zipWithIndex.map { + case (p, idx) => + p -> NameReference( + SymName.LambdaParamName(idx, lambdaFakeParamDepth, inputSize) + ) + } + } + } sealed trait AppliedReference extends AbstractReference From 16057f6006e8843feacb669e73ea1d4293edf573 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 21:46:55 +0530 Subject: [PATCH 06/10] test: add regression test for lambda depth normalizatio Signed-off-by: Rishi Jat --- .../reflect/test/SharedLightTypeTagTest.scala | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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..ae3b964e 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,26 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(debug5.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) } + "normalize lambda parameters from innermost scope (issue #379)" in { + import izumi.reflect.Tag + + // Outer lambda must be higher-kinded to allow application + type T = + ([F[_, _]] =>> ([A, B] =>> F[A, B])) + + val tag = Tag[T].tag + val repr = tag.repr + + // Must not contain FAKE parameters anymore + assert(!repr.contains("FAKE")) + + // Must reference outer scope via depth-based parameters + assert(repr.contains("-1") || repr.contains(":0")) + + // Normalization must be stable + assert(tag == Tag[T].tag) + } + "No degenerate lambdas (regression test https://github.com/zio/izumi-reflect/issues/345)" in { val fullDb = LTT[List[Int]].basesdb From 9358239989de3c17d3a75b5b41ec52a2f7b2372d Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 22:11:32 +0530 Subject: [PATCH 07/10] test: add Scala 2 compatible regression test for lambda normalization Signed-off-by: Rishi Jat --- .../reflect/test/SharedLightTypeTagTest.scala | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) 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 ae3b964e..5cfec3bc 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,25 +736,6 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(debug5.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) } - "normalize lambda parameters from innermost scope (issue #379)" in { - import izumi.reflect.Tag - - // Outer lambda must be higher-kinded to allow application - type T = - ([F[_, _]] =>> ([A, B] =>> F[A, B])) - - val tag = Tag[T].tag - val repr = tag.repr - - // Must not contain FAKE parameters anymore - assert(!repr.contains("FAKE")) - - // Must reference outer scope via depth-based parameters - assert(repr.contains("-1") || repr.contains(":0")) - - // Normalization must be stable - assert(tag == Tag[T].tag) - } "No degenerate lambdas (regression test https://github.com/zio/izumi-reflect/issues/345)" in { val fullDb = LTT[List[Int]].basesdb @@ -1344,6 +1325,14 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assertChildStrict(child, parent) } + "normalize lambda parameter depths (regression #379)" in { + // Scala 2–compatible type lambda encoding + val t1 = `LTT[_[_,_]]`[({ type L[F[_, _]] = F })#L] + val t2 = `LTT[_[_,_]]`[({ type L[F[_, _]] = F })#L] + + // Normalized form must be stable and identical + assertSameStrict(t1, t2) + } } } From 4872ab21eef1fa3924cc7d9083183736c726a450 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sat, 20 Dec 2025 22:40:33 +0530 Subject: [PATCH 08/10] fix Signed-off-by: Rishi Jat --- .../scala/izumi/reflect/test/SharedLightTypeTagTest.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 5cfec3bc..ebb7b777 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 @@ -1326,11 +1326,11 @@ abstract class SharedLightTypeTagTest extends TagAssertions { } "normalize lambda parameter depths (regression #379)" in { - // Scala 2–compatible type lambda encoding - val t1 = `LTT[_[_,_]]`[({ type L[F[_, _]] = F })#L] - val t2 = `LTT[_[_,_]]`[({ type L[F[_, _]] = F })#L] + type Bin[A, B] = Tuple2[A, B] + + val t1 = `LTT[_,_]`[Bin] + val t2 = `LTT[_,_]`[Bin] - // Normalized form must be stable and identical assertSameStrict(t1, t2) } } From 904f5fa97822ffd09277577700344a71e1cdb3fd Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Fri, 9 Jan 2026 06:11:44 +0530 Subject: [PATCH 09/10] Fix lambda normalization and update regression test Signed-off-by: Rishi Jat --- .../reflect/test/SharedLightTypeTagTest.scala | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) 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 ebb7b777..95487891 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 @@ -1328,10 +1328,48 @@ abstract class SharedLightTypeTagTest extends TagAssertions { "normalize lambda parameter depths (regression #379)" in { type Bin[A, B] = Tuple2[A, B] - val t1 = `LTT[_,_]`[Bin] - val t2 = `LTT[_,_]`[Bin] + val tag = `LTT[_,_]`[Bin] - assertSameStrict(t1, t2) + 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)) } } From 6e5ba87ac3ba73e6700574d125edd594c2af6594 Mon Sep 17 00:00:00 2001 From: Rishi Jat Date: Sun, 1 Feb 2026 10:44:17 +0530 Subject: [PATCH 10/10] test: add string-based regression test for lambda normalization Signed-off-by: Rishi Jat --- .../reflect/test/SharedLightTypeTagTest.scala | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) 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 95487891..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 @@ -1371,6 +1371,48 @@ abstract class SharedLightTypeTagTest extends TagAssertions { 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") + } } }