Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand Down Expand Up @@ -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 {
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
}
}

}