From 05ee30de6bdd0fb1521731edcf144faaba358653 Mon Sep 17 00:00:00 2001 From: Pavel Shirshov Date: Mon, 23 Feb 2026 18:59:02 +0000 Subject: [PATCH 1/5] lambda normalization --- .../main/scala-2/izumi/reflect/TagMacro.scala | 2 +- .../reflect/macrortti/LightTypeTagImpl.scala | 10 +- .../main/scala-3/izumi/reflect/TagMacro.scala | 2 +- .../dottyreflection/FullDbInspector.scala | 33 +++- .../reflect/dottyreflection/Inspector.scala | 4 +- .../izumi/reflect/macrortti/LTTOrdering.scala | 2 +- .../izumi/reflect/macrortti/LambdaNorm.scala | 162 ++++++++++++++++++ .../reflect/macrortti/LightTypeTag.scala | 8 +- .../macrortti/LightTypeTagInheritance.scala | 149 ++++++++++++++-- .../reflect/macrortti/LightTypeTagRef.scala | 47 ++--- .../izumi/reflect/macrortti/RuntimeAPI.scala | 9 +- .../reflect/test/LTTRenderablesTest.scala | 7 +- .../SharedLightTypeTagProgressionTest.scala | 14 +- .../reflect/test/SharedLightTypeTagTest.scala | 32 +--- 14 files changed, 386 insertions(+), 95 deletions(-) create mode 100644 izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala diff --git a/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/TagMacro.scala b/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/TagMacro.scala index 57832a70..657d788f 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/TagMacro.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/TagMacro.scala @@ -249,7 +249,7 @@ class TagMacro(val c: blackbox.Context) { val firstParamIdx = 0 val ctorLambdaParameter = SymName.LambdaParamName(firstParamIdx, LightTypeTagRef.LambdaConstants.tagMacro, arity) - val ctorApplyingLambda = LightTypeTagRef.Lambda( + val ctorApplyingLambda = LightTypeTagRef.Lambda.make( ctorLambdaParameter :: usageOrderDistinctNonLambdaArgs ::: declarationOrderLambdaParamArgs, FullReference(ctorLambdaParameter, usages) ) diff --git a/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/macrortti/LightTypeTagImpl.scala b/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/macrortti/LightTypeTagImpl.scala index 25b61075..5f7290d2 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/macrortti/LightTypeTagImpl.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-2/izumi/reflect/macrortti/LightTypeTagImpl.scala @@ -338,7 +338,7 @@ final class LightTypeTagImpl[U <: Universe with Singleton](val u: U, withCache: case l: Lambda => l case applied: AppliedReference => - val l = Lambda(lambdaParamsUnpacked, applied) + val l = Lambda.make(lambdaParamsUnpacked, applied) // Some(l).filter(_.allArgumentsReferenced) // do not include non-lambda parents such as Product into lambda's inheritance tree // No, include ALL bases for lambdas (this should be more correct since lambda is a template for a full parameterized db after combine) if (l.someArgumentsReferenced) l else applied @@ -478,12 +478,12 @@ final class LightTypeTagImpl[U <: Universe with Singleton](val u: U, withCache: val polyTypeResult = Dealias.fullNormDealiasSquashHKTToPolyTypeResultType(polyType) val tparams = polyType.typeParams - val nestingLevel = if (level > 0) Some(level) else None - val lambdaParams = makeLambdaParams(nestingLevel, tparams) + assert(level >= 0) + val lambdaParams = makeLambdaParams(Some(level), tparams) thisLevel.log(s"✴️ λ type $t has parameters $lambdaParams and result $polyTypeResult terminal names = $terminalNames") val reference = makeRefSub(polyTypeResult, lambdaParams.toMap, Set.empty) - val out = Lambda(lambdaParams.map(_._2), reference) + val out = Lambda.make(lambdaParams.map(_._2), reference) if (!out.allArgumentsReferenced) { val kvParams = lambdaParams.map { case (k, v) => s"$v = $k" } thisLevel.log( @@ -597,7 +597,7 @@ final class LightTypeTagImpl[U <: Universe with Singleton](val u: U, withCache: val out = tpe0 match { case l if isLambdaOutput => // this is required for handling SwapF2, etc. IzAssert(!isHKTOrPolyType(l), l -> l.getClass) - val out = Lambda(terminalNames.values.toList, unpackAsProperType(l, terminalNames)) + val out = Lambda.make(terminalNames.values.toList, unpackAsProperType(l, terminalNames)) out case l: PolyTypeApi => diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/TagMacro.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/TagMacro.scala index fd9e6f0b..b8ba0510 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/TagMacro.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/TagMacro.scala @@ -117,7 +117,7 @@ final class TagMacro(using override val qctx: Quotes) extends InspectorBase { val ctorLambdaParameter = SymName.LambdaParamName(firstParamIdx, LightTypeTagRef.LambdaConstants.tagMacro, arity) val ctorApplyingLambda = - LightTypeTagRef.Lambda( + LightTypeTagRef.Lambda.make( ctorLambdaParameter :: completeTail, FullReference(ctorLambdaParameter, usages) ) diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala index 0005352e..ca88b940 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala @@ -1,7 +1,7 @@ package izumi.reflect.dottyreflection import izumi.reflect.internal.fundamentals.collections.IzCollections.toRich -import izumi.reflect.macrortti.LightTypeTagRef +import izumi.reflect.macrortti.{LightTypeTagRef, RuntimeAPI} import izumi.reflect.macrortti.LightTypeTagRef.* import scala.collection.immutable.Queue @@ -42,9 +42,11 @@ abstract class FullDbInspector(protected val shift: Int) extends InspectorBase { extractBase(appliedType, selfRef(), onlyIndirect = onlyIndirect) ++ extractLambdaBase(appliedType, onlyIndirect = onlyIndirect) case typeLambda: TypeLambda => - val resultTypeParents = new Run(inspector.nextLam(typeLambda), basesTermination, toLambdaTermination).inspectTypeBoundsToFull(typeLambda.resType) + val innerInspector = inspector.nextLam(typeLambda) + val rawParams = innerInspector.context.last.params.map(_.asParam) + val resultTypeParents = new Run(innerInspector, basesTermination, toLambdaTermination).inspectTypeBoundsToFull(typeLambda.resType) - makeLambdaParents(selfRef(), resultTypeParents) + makeLambdaParents(selfRef(), rawParams, resultTypeParents) case a: AndType => inspectTypeReprToFullBases(a.left, onlyIndirect = false) ++ inspectTypeReprToFullBases(a.right, onlyIndirect = false) @@ -91,22 +93,39 @@ abstract class FullDbInspector(protected val shift: Int) extends InspectorBase { case Some(typeLambda) => toLambdaTermination.addTerminatingClsSym(appliedType) - val resultTypeParents = new Run(inspector.nextLam(typeLambda), basesTermination, toLambdaTermination) + val innerInspector = inspector.nextLam(typeLambda) + val rawParams = innerInspector.context.last.params.map(_.asParam) + val resultTypeParents = new Run(innerInspector, basesTermination, toLambdaTermination) .inspectTypeBoundsToFull(typeLambda.resType) - makeLambdaParents(inspector.inspectTypeRepr(typeLambda), resultTypeParents) + makeLambdaParents(inspector.inspectTypeRepr(typeLambda), rawParams, resultTypeParents) } } } private def makeLambdaParents( selfRef: AbstractReference, + rawParams: List[SymName.LambdaParamName], resultTypeParents: List[(AbstractReference, AbstractReference)] ): List[(AbstractReference, AbstractReference)] = { val selfL = selfRef.asInstanceOf[Lambda] + // resultTypeParents use raw (non-normalized) param names from the Inspector context. + // selfL has been normalized by Lambda.make. We need to rewrite the raw param names + // in resultTypeParents to match selfL's normalized param names. + val paramMapping: Map[SymName.LambdaParamName, AbstractReference] = + rawParams.zip(selfL.input).collect { + case (raw, norm) if raw != norm => raw -> LightTypeTagRef.NameReference(norm) + }.toMap + val rewriter = new RuntimeAPI.Rewriter(paramMapping) + def normalizeEntry(ref: AbstractReference): AbstractReference = + if (paramMapping.isEmpty) ref else rewriter.replaceRefs(ref) + val out = resultTypeParents.flatMap { - case (child0, parent0) => + case (child0raw, parent0raw) => + val child0 = normalizeEntry(child0raw) + val parent0 = normalizeEntry(parent0raw) + val child = if (child0 == selfL.output) { // if child == typeLambda.resType, use typeLambda itself selfL } else { @@ -118,7 +137,7 @@ abstract class FullDbInspector(protected val shift: Int) extends InspectorBase { case l: Lambda => l case applied: AppliedReference => - val l = LightTypeTagRef.Lambda(selfL.input, applied) + val l = LightTypeTagRef.Lambda.make(selfL.input, applied) if (l.someArgumentsReferenced) l else applied } 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 4297a7e6..fa031bb4 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 @@ -108,7 +108,7 @@ abstract class Inspector(protected val shift: Int, val context: Queue[Inspector. val inspector = nextLam(l) val resType = inspector.inspectTypeRepr(l.resType) val paramNames = inspector.context.last.params.map(_.asParam) - LightTypeTagRef.Lambda(paramNames, resType) + LightTypeTagRef.Lambda.make(paramNames, resType) case t: ThisType => next().inspectTypeRepr(t.tref) @@ -238,7 +238,7 @@ abstract class Inspector(protected val shift: Int, val context: Queue[Inspector. // Upper boundaries we'll recover later in fulldb and inheritancedb // But lower boundaries we don't recover log(s"invertTypeMemberWithTypeLambdaBounds: found symName=$symName, input=$input") - LightTypeTagRef.Lambda(input, FullReference(symName, input.map(p => TypeParam(NameReference(p), Variance.Invariant)), prefix)) + LightTypeTagRef.Lambda.make(input, FullReference(symName, input.map(p => TypeParam(NameReference(p), Variance.Invariant)), prefix)) case other => other } diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LTTOrdering.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LTTOrdering.scala index 241aab36..3fb21bdf 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LTTOrdering.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LTTOrdering.scala @@ -36,7 +36,7 @@ private[macrortti] trait LTTOrdering { // Mirror Lambda#equals val compare1 = Ordering.Int.compare(lx.input.size, ly.input.size) if (compare1 != 0) return compare1 - OrderingAbstractReference.compare(lx.normalizedOutput, ly.normalizedOutput) + OrderingAbstractReference.compare(lx.output, ly.output) case (IntersectionReference(refsx), IntersectionReference(refsy)) => OrderingArrayAbstractReference.compare(refSetToSortedArray(refsx), refSetToSortedArray(refsy)) diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala new file mode 100644 index 00000000..1882b13a --- /dev/null +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala @@ -0,0 +1,162 @@ +package izumi.reflect.macrortti + +import izumi.reflect.macrortti.LightTypeTagRef._ + +private[macrortti] object LambdaNorm { + + def normalize(lambda: Lambda): Lambda = { + normalizeRef(lambda, env = Nil) match { + case l: Lambda => + l + case other => + throw new IllegalStateException(s"Expected Lambda after normalization, got: $other") + } + } + + def maxFreeVarDepth(lambda: Lambda, resolveDepth: SymName.LambdaParamName => Int = _.depth): Int = { + val bound = scala.collection.mutable.HashSet.empty[SymName.LambdaParamName] + var maxDepth = -1 + + def visitBoundaries(b: Boundaries): Unit = b match { + case Boundaries.Defined(bottom, top) => visit(bottom); visit(top) + case Boundaries.Empty => + } + + def checkFreeVar(lpn: SymName.LambdaParamName): Unit = { + if (!bound.contains(lpn) && lpn.depth >= 0) maxDepth = maxDepth.max(resolveDepth(lpn)) + } + + def visit(ref: AbstractReference): Unit = ref match { + case l: Lambda => + val added = l.input.filter(bound.add) + visit(l.output) + added.foreach(bound.remove) + case NameReference(lpn: SymName.LambdaParamName, boundaries, prefix) => + checkFreeVar(lpn) + visitBoundaries(boundaries) + prefix.foreach(visit) + case NameReference(_, boundaries, prefix) => + visitBoundaries(boundaries) + prefix.foreach(visit) + case FullReference(lpn: SymName.LambdaParamName, parameters, prefix) => + checkFreeVar(lpn) + parameters.foreach(p => visit(p.ref)) + prefix.foreach(visit) + case FullReference(_, parameters, prefix) => + parameters.foreach(p => visit(p.ref)) + prefix.foreach(visit) + case IntersectionReference(refs) => refs.foreach(visit) + case UnionReference(refs) => refs.foreach(visit) + case WildcardReference(boundaries) => visitBoundaries(boundaries) + case Refinement(base, decls) => + visit(base) + decls.foreach { + case RefinementDecl.Signature(_, in, out) => in.foreach(visit); visit(out) + case RefinementDecl.TypeMember(_, ref) => visit(ref) + } + } + + visit(lambda) + maxDepth + } + + private type Env = List[Map[SymName.LambdaParamName, SymName.LambdaParamName]] + + private def resolveParamName(paramName: SymName.LambdaParamName, env: Env): SymName.LambdaParamName = { + env.iterator.map(_.get(paramName)).collectFirst { case Some(mapped) => mapped }.getOrElse(paramName) + } + + private def resolveParamNamePreferOuter(paramName: SymName.LambdaParamName, env: Env): SymName.LambdaParamName = { + val matches = env.iterator.map(_.get(paramName)).collect { case Some(mapped) => mapped }.toList + matches.lastOption.getOrElse(paramName) + } + + private def normalizeSymName(symName: SymName, env: Env, preferOuter: Boolean = false): SymName = { + symName match { + case l: SymName.LambdaParamName => + if (preferOuter) resolveParamNamePreferOuter(l, env) else resolveParamName(l, env) + case other => + other + } + } + + private def normalizeBoundaries(boundaries: Boundaries, env: Env): Boundaries = { + boundaries match { + case Boundaries.Defined(bottom, top) => + Boundaries.Defined(normalizeRef(bottom, env), normalizeRef(top, env)) + case Boundaries.Empty => + Boundaries.Empty + } + } + + private def normalizeRef(reference: AbstractReference, env: Env): AbstractReference = { + reference match { + case l: Lambda => + val localFreeVarOffset = maxFreeVarDepth(l, lpn => resolveParamName(lpn, env).depth) + val normalizedDepth = localFreeVarOffset + 1 + val newArity = l.input.size + val localMap = l + .input.zipWithIndex.iterator.map { + case (param, idx) => + val d = if (param.depth < 0) param.depth else normalizedDepth + param -> param.copy(index = idx, depth = d, arity = newArity) + }.toMap + val normalizedInput = l.input.map(p => localMap.getOrElse(p, throw new IllegalStateException(s"Missing normalized param for $p"))) + val nextEnv = localMap :: env + new Lambda(normalizedInput, normalizeRef(l.output, nextEnv)) + case IntersectionReference(refs) => + IntersectionReference(refs.map(normalizeRef(_, env)).map { + case a: AppliedReferenceExceptIntersection => a + case other => throw new IllegalStateException(s"Expected AppliedReferenceExceptIntersection, got: $other") + }) + case UnionReference(refs) => + UnionReference(refs.map(normalizeRef(_, env)).map { + case a: AppliedReferenceExceptUnion => a + case other => throw new IllegalStateException(s"Expected AppliedReferenceExceptUnion, got: $other") + }) + case WildcardReference(boundaries) => + WildcardReference(normalizeBoundaries(boundaries, env)) + case Refinement(base, decls) => + val normalizedBase = normalizeRef(base, env) match { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + val normalizedDecls = decls.map { + case RefinementDecl.Signature(name, in, out) => + RefinementDecl.Signature( + name, + in.map(normalizeRef(_, env)).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + }, + normalizeRef(out, env) match { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ): RefinementDecl + case RefinementDecl.TypeMember(name, ref) => + RefinementDecl.TypeMember(name, normalizeRef(ref, env)): RefinementDecl + } + Refinement(normalizedBase, normalizedDecls) + case NameReference(ref, boundaries, prefix) => + NameReference( + normalizeSymName(ref, env), + normalizeBoundaries(boundaries, env), + prefix.map(normalizeRef(_, env)).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ) + case FullReference(symName, parameters, prefix) => + FullReference( + normalizeSymName(symName, env, preferOuter = true), + parameters.map(p => TypeParam(normalizeRef(p.ref, env), p.variance)), + prefix.map(normalizeRef(_, env)).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ) + } + } + +} 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..96a4cae5 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 @@ -875,7 +875,7 @@ object LightTypeTag { // been processed by conversion procedure, so it should // have no clashes in SymTypeName with old parameters // anymore and be safe to process - Lambda(input, goReplace(output)) + LightTypeTagRef.Lambda.make(input, goReplace(output)) case IntersectionReference(refs) => IntersectionReference(refs.map(goReplace)) @@ -912,7 +912,7 @@ object LightTypeTag { } val LightTypeTagRef.Lambda(convertedParams, oldResult) = lambda - LightTypeTagRef.Lambda(convertedParams, goReplace(oldResult)) + LightTypeTagRef.Lambda.make(convertedParams, goReplace(oldResult)) } import OldLambdaParameter.{OldLambdaParameter, oldLambdaParameterPickler} @@ -923,12 +923,12 @@ object LightTypeTag { val convertedParams = oldParams.map(SymName.bincompatForceCreateLambdaParamNameFromString(_)) val paramMap = oldParams.iterator.zip(convertedParams.iterator).toMap[String, SymName.LambdaParamName] - val oldLambda = LightTypeTagRef.Lambda(convertedParams, lambdaResult) + val oldLambda = LightTypeTagRef.Lambda.make(convertedParams, lambdaResult) val value = convertPre230LambdaToNew(oldLambda, paramMap) state.addIdentityRef(value) value } else if (ic == 1) { // Post 2.3.0 - val value = LightTypeTagRef.Lambda( + val value = LightTypeTagRef.Lambda.make( state.unpickle[List[SymName.LambdaParamName]], state.unpickle[LightTypeTagRef.AbstractReference] ) diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala index 1b4b1a5e..0c612458 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala @@ -295,10 +295,6 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { } else if (oneOfParameterizedParentsIsInheritedFrom(ctx)(self, that)) { true } else { - val selfNormalizedLambdaParamsSize = { - // 0 unless we're in a lambda vs. lambda comparison (s.normalizedOutput <:< t.normalizedOutput) - self.parameters.count(p => isFakeParam(p.ref)) - } val appliedLambdaParents = basesdb .iterator.collect { case (l: Lambda, lparents) if isSameNamedRef(l.output, selfNameRef) => @@ -311,12 +307,18 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { Nil } } - val scala3FullDbFormLambda = if (lp.input.size == selfNormalizedLambdaParamsSize) { - val ps = self.parameters.collect { case FakeParam(pRef) => pRef } - val selfPs = ps.sortBy(_.ref.asInstanceOf[SymName.LambdaParamName].index) - val res = lp.combine(selfPs) - List(res) - } else Nil + + val scala3FullDbFormLambda = { + // 0 unless we're in a lambda vs. lambda comparison (s.normalizedOutput <:< t.normalizedOutput) + val selfNormalizedLambdaParamsSize = self.parameters.count(p => isFakeParam(p.ref)) + if (lp.input.size == selfNormalizedLambdaParamsSize) { + val ps = self.parameters.collect { case FakeParam(pRef) => pRef } + val selfPs = ps.sortBy(_.ref.asInstanceOf[SymName.LambdaParamName].index) + val res = lp.combine(selfPs) + List(res) + } else Nil + } + scala2FullDbFormLambda ++ scala3FullDbFormLambda case _ => Nil @@ -325,7 +327,11 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { ctx.logger.log { s"ℹ️ checking applied lambda parents of self=`${self.repr}`: parents=${appliedLambdaParents.map(_.repr)} <:< that=`${that.repr}`" } - appliedLambdaParents.exists(ctx.isChild(_, that)) + val normalizedThat = normalizeLambdas(that) + appliedLambdaParents.exists { + p => + ctx.isChild(p, that) || ctx.isChild(normalizeLambdas(p), normalizedThat) + } } } @@ -353,19 +359,134 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { } } + private def normalizeLambdas(reference: AbstractReference): AbstractReference = { + reference match { + case l: Lambda => + l.normalize() + case IntersectionReference(refs) => + IntersectionReference(refs.map(normalizeLambdas).map { + case r: AppliedReferenceExceptIntersection => r + case other => throw new IllegalStateException(s"Expected AppliedReferenceExceptIntersection, got: $other") + }) + case UnionReference(refs) => + UnionReference(refs.map(normalizeLambdas).map { + case r: AppliedReferenceExceptUnion => r + case other => throw new IllegalStateException(s"Expected AppliedReferenceExceptUnion, got: $other") + }) + case WildcardReference(boundaries) => + WildcardReference( + boundaries match { + case Boundaries.Defined(bottom, top) => + Boundaries.Defined(normalizeLambdas(bottom), normalizeLambdas(top)) + case Boundaries.Empty => + Boundaries.Empty + } + ) + case Refinement(base, decls) => + val normalizedBase = normalizeLambdas(base) match { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + val normalizedDecls = decls.map { + case RefinementDecl.Signature(name, input, output) => + RefinementDecl.Signature( + name, + input.map(normalizeLambdas).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + }, + normalizeLambdas(output) match { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ): RefinementDecl + case RefinementDecl.TypeMember(name, ref) => + RefinementDecl.TypeMember(name, normalizeLambdas(ref)): RefinementDecl + } + Refinement(normalizedBase, normalizedDecls) + case NameReference(ref, boundaries, prefix) => + NameReference( + ref, + boundaries match { + case Boundaries.Defined(bottom, top) => + Boundaries.Defined(normalizeLambdas(bottom), normalizeLambdas(top)) + case Boundaries.Empty => + Boundaries.Empty + }, + prefix.map(normalizeLambdas).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ) + case FullReference(symName, parameters, prefix) => + FullReference( + symName, + parameters.map(p => TypeParam(normalizeLambdas(p.ref), p.variance)), + prefix.map(normalizeLambdas).map { + case a: AppliedReference => a + case other => throw new IllegalStateException(s"Expected AppliedReference, got: $other") + } + ) + } + } + private def parameterizedParentsOf(t: AbstractReference): Set[AbstractReference] = { val basesDbParents = basesdb.getOrElse(t, Set.empty) + val templateParents = t match { + case actual: FullReference => + resolveTemplateParameterizedParents(actual) + case _ => + Set.empty[AbstractReference] + } + val allParents = basesDbParents ++ templateParents val withBoundaryParents = t match { case NameReference(_, b: Boundaries.Defined, _) => - basesDbParents + b.top + allParents + b.top case WildcardReference(b: Boundaries.Defined) => - basesDbParents + b.top + allParents + b.top case _ => - basesDbParents + allParents } withBoundaryParents } + private def resolveTemplateParameterizedParents(actual: FullReference): Set[AbstractReference] = { + basesdb.iterator.collect { + case (template: FullReference, templateParents) => + buildTemplateSubstitution(template, actual).map { + substitution => + val rewriter = new RuntimeAPI.Rewriter(substitution) + templateParents.map(rewriter.replaceRefs) + } + }.flatten.flatten.toSet + } + + private def buildTemplateSubstitution( + template: FullReference, + actual: FullReference + ): Option[Map[SymName.LambdaParamName, AbstractReference]] = { + if (template.asName != actual.asName || template.parameters.size != actual.parameters.size) { + None + } else { + template.parameters.zip(actual.parameters).foldLeft(Option(Map.empty[SymName.LambdaParamName, AbstractReference])) { + case (None, _) => + None + case (Some(subst), (templateParam, actualParam)) => + templateParam.ref match { + case NameReference(lambdaParam: SymName.LambdaParamName, Boundaries.Empty, None) => + subst.get(lambdaParam) match { + case Some(existing) => + if (existing == actualParam.ref) Some(subst) else None + case None => + Some(subst.updated(lambdaParam, actualParam.ref)) + } + case otherRef => + if (otherRef == actualParam.ref) Some(subst) else None + } + } + } + } + private def oneOfParameterizedParentsIsInheritedFrom(ctx: Ctx)(child: AbstractReference, parent: AbstractReference): Boolean = { // ctx.logger.log(s"Checking if ${parameterizedParentsOf(child)} has a parent of $parent") val parents = parameterizedParentsOf(child) 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 1e6f5413..fce741c5 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 @@ -75,9 +75,22 @@ object LightTypeTagRef extends LTTOrdering { } } - final case class Lambda(input: List[SymName.LambdaParamName], output: AbstractReference) extends AbstractReference { + final case class Lambda private[macrortti] (input: List[SymName.LambdaParamName], output: AbstractReference) extends AbstractReference { + def normalize(): Lambda = LambdaNorm.normalize(this) + override def hashCode(): Int = { - normalizedOutput.hashCode() + inputSize * 31 + output.hashCode() + } + + override def equals(obj: Any): Boolean = { + obj match { + case l: Lambda => + inputSize == l.inputSize && + input == l.input && + output == l.output + case _ => + false + } } lazy val inputSize: Int = input.size @@ -89,39 +102,27 @@ object LightTypeTagRef extends LTTOrdering { // (Except possibly lower bound of an abstract/opaque type member) NameReference(n) }.toSet - lazy val referenced: Set[NameReference] = RuntimeAPI.unpack(this) + lazy val referenced: Set[NameReference] = RuntimeAPI.unpack(output) def allArgumentsReferenced: Boolean = paramRefs.diff(referenced).isEmpty lazy val someArgumentsReferenced: Boolean = { val unusedParamsSize = paramRefs.diff(referenced).size unusedParamsSize < paramRefs.size } - lazy val normalizedParams: List[NameReference] = makeFakeParams.map(_._2) - lazy val normalizedOutput: AbstractReference = RuntimeAPI.applyLambda(this, makeFakeParams) - - override def equals(obj: Any): Boolean = { - obj match { - case l: Lambda => - inputSize == l.inputSize && - (normalizedOutput == l.normalizedOutput) - - case _ => - false - } - } + lazy val normalizedOutput: AbstractReference = output + } - private[this] def makeFakeParams: List[(LambdaParamName, NameReference)] = { - input.zipWithIndex.map { - case (p, idx) => - p -> NameReference(SymName.LambdaParamName(idx, LightTypeTagRef.LambdaConstants.lambdaFakeParamDepth, inputSize)) // s"!FAKE_$idx" - } + object Lambda { + def make(input: List[SymName.LambdaParamName], output: AbstractReference): Lambda = { + new Lambda(input, output).normalize() } } object LambdaConstants { - final val defaultContextId = -1 + final val defaultContextId = 0 + final val tagMacro = defaultContextId + final val lambdaFakeParamDepth: Int = -2 // depth is always positive, unless fake - final val tagMacro = -3 } sealed trait AppliedReference extends AbstractReference diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/RuntimeAPI.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/RuntimeAPI.scala index e30ac116..4157f5b8 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/RuntimeAPI.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/RuntimeAPI.scala @@ -35,8 +35,9 @@ object RuntimeAPI { } ref match { - case Lambda(_, output) => - unpack(output) + case Lambda(input, output) => + val bound = input.iterator.map(p => NameReference(p): NameReference).toSet + unpack(output).diff(bound) case reference: AppliedReference => reference match { case reference: AppliedNamedReference => @@ -87,7 +88,7 @@ object RuntimeAPI { val res = if (newParams.isEmpty) { replaced } else { - val out = Lambda(newParams, replaced) + val out = Lambda.make(newParams, replaced) // assert(out.allArgumentsReferenced, s"bad lambda: $out, ${out.paramRefs}, ${out.referenced}") // such lambdas are legal: see "regression test: combine Const Lambda to TagK" out @@ -109,7 +110,7 @@ object RuntimeAPI { case l: Lambda => val bad = l.input.iterator.toSet val fixed = new Rewriter(_rules.filterKeys(!bad.contains(_)).iterator.toMap).replaceRefs(l.output) - l.copy(output = fixed) + LightTypeTagRef.Lambda.make(l.input, fixed) case o: AppliedReference => replaceApplied(o) diff --git a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/LTTRenderablesTest.scala b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/LTTRenderablesTest.scala index c6c67a3b..17ec6a5c 100644 --- a/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/LTTRenderablesTest.scala +++ b/izumi-reflect/izumi-reflect/src/test/scala/izumi/reflect/test/LTTRenderablesTest.scala @@ -44,10 +44,9 @@ class LTTRenderablesTest extends TagAssertions { assert(identity == "[A] ➾ A") assert(const == "[A,B] ➾ B") assert(swapEither == "[A,B] ➾ scala.util.Either[+B,+A]") - val expectedDepth = if (IsScala3) 1 else 2 - assert(swapOptionT == s"izumi.reflect.test.OptionT[[A$expectedDepth] ➾ A$expectedDepth,_]") - assert(useInner == s"[A] ➾ izumi.reflect.test.OptionT[[A$expectedDepth] ➾ scala.util.Either[+A,+A$expectedDepth],A]") - assert(useInner2 == s"[A,B] ➾ izumi.reflect.test.OptionT[[A$expectedDepth] ➾ scala.util.Either[+A,+A$expectedDepth],B]") + assert(swapOptionT == s"izumi.reflect.test.OptionT[[A] ➾ A,_]") + assert(useInner == s"[A] ➾ izumi.reflect.test.OptionT[[A1] ➾ scala.util.Either[+A,+A1],A]") + assert(useInner2 == s"[A,B] ➾ izumi.reflect.test.OptionT[[A1] ➾ scala.util.Either[+A,+A1],B]") assert(reuse == "[A] ➾ scala.util.Either[+A,+A]") } 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..5cac6142 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 @@ -53,15 +53,17 @@ abstract class SharedLightTypeTagProgressionTest extends TagAssertions with TagP // def compare[a >: c <: b, b <: d, c <: d, d, A[x >: a <: b] <: B[x], B[_ >: c <: d], x >: a <: b](s: A[x], t: B[_ >: c <: d]) = null - broken { -//// compare[H5, H5, H4, H2, X2, X, H5](null: Set[H5], null) // error -//// (null: Set[H5]): Set[_ >: H4 <: H2] // error + brokenOnScala3 { +// compare[H5, H5, H4, H2, X2, X, H5](null: Set[H5], null) // error +// (null: Set[H5]): Set[_ >: H4 <: H2] // error assertNotChild(`LTT[A,B,_>:B<:A]`[H5, H5, X2], `LTT[A,B,_>:B<:A]`[H2, H4, X]) } // compare[H3, H3, H4, H2, X1, X, H3](null: Set[H3], null) // (null: Set[H3]): Set[_ >: H4 <: H2] - assertChild(`LTT[A,B,_>:B<:A]`[H3, H3, X1], `LTT[A,B,_>:B<:A]`[H2, H4, X]) + brokenOnScala2 { + assertChild(`LTT[A,B,_>:B<:A]`[H3, H3, X1], `LTT[A,B,_>:B<:A]`[H2, H4, X]) + } } "progression test: indirect structural checks do not work" in { @@ -103,10 +105,10 @@ abstract class SharedLightTypeTagProgressionTest extends TagAssertions with TagP 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 → scala.util.Right[+java.lang.Throwable,+0]")) assert(debug1.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) broken { - assert(!debug1.contains("λ %1 → scala.util.Right[+scala.Unit,+1]")) + assert(!debug1.contains("λ %0 → scala.util.Right[+scala.Unit,+0]")) } } 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..ecbd8df9 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 @@ -695,11 +695,7 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(!debug3.contains("TestModel.A")) assert(!debug3.contains("+scala.Nothing")) // assert(debug3.contains("- λ %0 → scala.util.Right[+java.lang.Throwable,+0]")) - if (IsScala3) { - assert(debug3.contains("- λ %1:0,%1:1 → scala.util.Right[+1:0,+1:1]")) - } else { - assert(debug3.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) - } + assert(debug3.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) assert(debug3.contains("* scala.Product")) val debug4 = `LTT[_]`[Right[Throwable, *]].debug() @@ -715,11 +711,7 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(!debug4.contains("TestModel.A")) assert(!debug4.contains("+scala.Nothing")) // assert(debug4.contains("- λ %0 → scala.util.Right[+java.lang.Throwable,+0]")) - if (IsScala3) { - assert(debug4.contains("- λ %1:0,%1:1 → scala.util.Right[+1:0,+1:1]")) - } else { - assert(debug4.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) - } + assert(debug4.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) assert(debug4.contains("* scala.Product")) val oneArgApplied = `LTT[_,_]`[Right].combine(LTT[Throwable]).combine(LTT[Unit]) @@ -732,7 +724,7 @@ abstract class SharedLightTypeTagTest extends TagAssertions { assert(!debug5.contains("scala.package.B")) assert(!debug5.contains("+scala.Nothing")) assert(debug5.contains("* scala.Product")) - assert(debug5.contains("- λ %1 → scala.util.Right[+java.lang.Throwable,+1]")) + assert(debug5.contains("- λ %0 → scala.util.Right[+java.lang.Throwable,+0]")) assert(debug5.contains("- λ %0,%1 → scala.util.Right[+0,+1]")) } @@ -937,24 +929,18 @@ abstract class SharedLightTypeTagTest extends TagAssertions { type TX[B] = Int { def a(k: String): Int; val b: String; type M1 = W1; type M2 <: W2; type M3[A] = Either[B, A] } val txTag = `LTT[_]`[TX] assert( - (txTag.toString // Scala 2 - == "λ %0 → (Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %2:0 → Either[+0,+2:0]})") - || (txTag.toString // Dotty - == "λ %0 → (Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %1:0 → Either[+0,+1:0]})") + txTag.toString + == "λ %0 → (Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %1:0 → Either[+0,+1:0]})" ) val txCombinedTag = `LTT[_]`[TX].combine(LTT[Unit]) assert( - (txCombinedTag.toString // Scala 2 - == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %2:0 → Either[+Unit,+2:0]})") - || (txCombinedTag.toString // Dotty - == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %1:0 → Either[+Unit,+1:0]})") + txCombinedTag.toString + == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %0 → Either[+Unit,+0]})" ) val txUnitTag = LTT[TX[Unit]] assert( - (txUnitTag.toString - == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %1:0 → Either[+Unit,+1:0]})") - || (txUnitTag.toString - == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %0 → Either[+Unit,+0]})") + txUnitTag.toString + == "(Int {def a(String): Int, def b(): String, type M1 = TestModel::W1, type M2 = M2|, type M3 = λ %0 → Either[+Unit,+0]})" ) assertRepr(LTT[I1 with (I1 with (I1 with W1))], "{TestModel::I1 & TestModel::W1}") assertRepr(`LTT[_]`[R1], "λ %0 → TestModel::R1[=0]") From 72494f5dda527162d92d7de9f5ddf0730e26cfce Mon Sep 17 00:00:00 2001 From: Pavel Shirshov Date: Mon, 23 Feb 2026 19:35:56 +0000 Subject: [PATCH 2/5] scala 3: bounds support --- .../dottyreflection/FullDbInspector.scala | 10 +- .../reflect/dottyreflection/Inspector.scala | 20 ++- .../macrortti/LightTypeTagInheritance.scala | 120 +++++++++++++++++- .../SharedLightTypeTagProgressionTest.scala | 19 +-- 4 files changed, 148 insertions(+), 21 deletions(-) diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala index ca88b940..53dd9be5 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/dottyreflection/FullDbInspector.scala @@ -57,9 +57,13 @@ abstract class FullDbInspector(protected val shift: Int) extends InspectorBase { case typeRef: TypeRef => processSymbol(typeRef, selfRef(), onlyIndirect = onlyIndirect) - case _: ParamRef => - // do not process type parameters for bases db - Nil + case p: ParamRef => + // inspect bounds of type parameters to capture parent relationships + // for types referenced in lambda param boundaries (e.g. [A >: H4 <: H2]) + p._underlying match { + case tb: TypeBounds => processTypeBounds(tb) + case _ => Nil + } case termRef: TermRef => extractBase(termRef, selfRef(), onlyIndirect = onlyIndirect) 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 fa031bb4..eb6ed769 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 @@ -246,13 +246,25 @@ abstract class Inspector(protected val shift: Int, val context: Queue[Inspector. private def inspectBoundsImpl(tb: TypeBounds): Boundaries = { val hi = next().inspectTypeRepr(tb.hi) val low = next().inspectTypeRepr(tb.low) - if (hi == LightTypeTagInheritance.tpeAny && low == LightTypeTagInheritance.tpeNothing) { + if (isTrivialLowerBound(low) && isTrivialUpperBound(hi)) { Boundaries.Empty } else { Boundaries.Defined(low, hi) } } + private def isTrivialLowerBound(ref: AbstractReference): Boolean = ref match { + case n: NameReference => n == LightTypeTagInheritance.tpeNothing + case LightTypeTagRef.Lambda(_, output) => isTrivialLowerBound(output) + case _ => false + } + + private def isTrivialUpperBound(ref: AbstractReference): Boolean = ref match { + case n: NameReference => n == LightTypeTagInheritance.tpeAny + case LightTypeTagRef.Lambda(_, output) => isTrivialUpperBound(output) + case _ => false + } + private[dottyreflection] def inspectSymbol(symbol: Symbol, outerTypeRef: Option[TypeRef], prefixSource: Option[NamedType]): AbstractReference = { symbol match { case s if s.isClassDef || s.isValDef || s.isBind => @@ -342,7 +354,11 @@ abstract class Inspector(protected val shift: Int, val context: Queue[Inspector. assert(contextParam.name == paramName, s"$contextParam should match $paramName") } - NameReference(contextParam.asParam, Boundaries.Empty, None) + val boundaries = t._underlying match { + case tb: TypeBounds => inspectBoundsImpl(tb) + case _ => Boundaries.Empty + } + NameReference(contextParam.asParam, boundaries, None) } else { val lt = t.binder.asInstanceOf[LambdaType] diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala index 0c612458..b48b8736 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala @@ -150,8 +150,21 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { case (s: Lambda, t: AppliedNamedReference) => isChild(ctx.next())(s.output, t) case (s: Lambda, o: Lambda) => - (s.input.size == o.input.size - && isChild(ctx.next())(s.normalizedOutput, o.normalizedOutput)) + s.input.size == o.input.size && { + val sBoundsMap = collectLambdaParamBoundaries(s.output, s.input.toSet) + val oBoundsMap = collectLambdaParamBoundaries(o.output, o.input.toSet) + val boundsOk = s.input.zip(o.input).forall { + case (sp, op) => + val sBounds = sBoundsMap.getOrElse(sp, Boundaries.Empty) + val oBounds = oBoundsMap.getOrElse(op, Boundaries.Empty) + compareLambdaParamBounds(ctx)(sBounds, oBounds) + } + boundsOk && { + val sOut = stripLambdaParamBoundaries(s.normalizedOutput, s.input.toSet) + val oOut = stripLambdaParamBoundaries(o.normalizedOutput, o.input.toSet) + isChild(ctx.next())(sOut, oOut) + } + } // intersections case (s: IntersectionReference, t: IntersectionReference) => @@ -204,6 +217,109 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { } } + /** For lambda subtyping `[A >: L1 <: U1] => T1 <: [B >: L2 <: U2] => T2`, + * the child's bounds must be at least as wide as the parent's: L1 <: L2 and U2 <: U1. + */ + private def compareLambdaParamBounds(ctx: Ctx)(childBounds: Boundaries, parentBounds: Boundaries): Boolean = { + (childBounds, parentBounds) match { + case (_, Boundaries.Empty) => + true + case (Boundaries.Empty, Boundaries.Defined(_, _)) => + true // unbounded child accepts everything + case (Boundaries.Defined(cLo, cHi), Boundaries.Defined(pLo, pHi)) => + ctx.isChild(pLo, cLo) && ctx.isChild(cHi, pHi) + } + } + + private def collectLambdaParamBoundaries( + ref: AbstractReference, + params: Set[SymName.LambdaParamName] + ): Map[SymName.LambdaParamName, Boundaries] = { + val result = mutable.HashMap.empty[SymName.LambdaParamName, Boundaries] + + def collectBoundaries(b: Boundaries): Unit = b match { + case Boundaries.Defined(bottom, top) => visit(bottom); visit(top) + case Boundaries.Empty => + } + + def visit(r: AbstractReference): Unit = r match { + case NameReference(lpn: SymName.LambdaParamName, boundaries, prefix) => + if (params.contains(lpn) && !result.contains(lpn) && boundaries != Boundaries.Empty) { + result(lpn) = boundaries + } + collectBoundaries(boundaries) + prefix.foreach(visit) + case NameReference(_, boundaries, prefix) => + collectBoundaries(boundaries) + prefix.foreach(visit) + case FullReference(_, parameters, prefix) => + parameters.foreach(p => visit(p.ref)) + prefix.foreach(visit) + case Lambda(_, output) => + visit(output) + case IntersectionReference(refs) => + refs.foreach(visit) + case UnionReference(refs) => + refs.foreach(visit) + case WildcardReference(boundaries) => + collectBoundaries(boundaries) + case Refinement(base, decls) => + visit(base) + decls.foreach { + case RefinementDecl.Signature(_, input, output) => input.foreach(visit); visit(output) + case RefinementDecl.TypeMember(_, r) => visit(r) + } + } + + visit(ref) + result.toMap + } + + private def stripLambdaParamBoundaries( + ref: AbstractReference, + params: Set[SymName.LambdaParamName] + ): AbstractReference = ref match { + case NameReference(lpn: SymName.LambdaParamName, _, prefix) if params.contains(lpn) => + NameReference(lpn, Boundaries.Empty, prefix.map(p => stripLambdaParamBoundaries(p, params).asInstanceOf[AppliedReference])) + case NameReference(r, boundaries, prefix) => + NameReference(r, stripBoundaries(boundaries, params), prefix.map(p => stripLambdaParamBoundaries(p, params).asInstanceOf[AppliedReference])) + case FullReference(symName, parameters, prefix) => + FullReference( + symName, + parameters.map(p => TypeParam(stripLambdaParamBoundaries(p.ref, params), p.variance)), + prefix.map(p => stripLambdaParamBoundaries(p, params).asInstanceOf[AppliedReference]) + ) + case Lambda(input, output) => + new Lambda(input, stripLambdaParamBoundaries(output, params)) + case IntersectionReference(refs) => + IntersectionReference(refs.map(r => stripLambdaParamBoundaries(r, params).asInstanceOf[AppliedReferenceExceptIntersection])) + case UnionReference(refs) => + UnionReference(refs.map(r => stripLambdaParamBoundaries(r, params).asInstanceOf[AppliedReferenceExceptUnion])) + case WildcardReference(boundaries) => + WildcardReference(stripBoundaries(boundaries, params)) + case Refinement(base, decls) => + Refinement( + stripLambdaParamBoundaries(base, params).asInstanceOf[AppliedReference], + decls.map { + case RefinementDecl.Signature(name, input, output) => + RefinementDecl.Signature( + name, + input.map(i => stripLambdaParamBoundaries(i, params).asInstanceOf[AppliedReference]), + stripLambdaParamBoundaries(output, params).asInstanceOf[AppliedReference] + ): RefinementDecl + case RefinementDecl.TypeMember(name, r) => + RefinementDecl.TypeMember(name, stripLambdaParamBoundaries(r, params)): RefinementDecl + } + ) + } + + private def stripBoundaries(boundaries: Boundaries, params: Set[SymName.LambdaParamName]): Boundaries = boundaries match { + case Boundaries.Defined(bottom, top) => + Boundaries.Defined(stripLambdaParamBoundaries(bottom, params), stripLambdaParamBoundaries(top, params)) + case Boundaries.Empty => + Boundaries.Empty + } + private def compareDecls(ctx: Ctx)(sDecls: Set[RefinementDecl], tDecls: Set[RefinementDecl]): Boolean = { val s = sDecls.groupBy(_.name) // for every decl on the right there's a <: decl on the left 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 5cac6142..e672de67 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 @@ -45,25 +45,16 @@ abstract class SharedLightTypeTagProgressionTest extends TagAssertions with TagP } } - "progression test: bounds-based subtype checks for lambdas do not work properly (LambdaParameter must contain bounds and NameReferences shouldn't for this to work)" in { - // I consider this stuff practically useless + "bounds-based subtype checks for lambdas work properly" in { type X[A >: H4 <: H2] = Set[A] type X1[A >: H3 <: H3] = Set[A] type X2[A >: H5 <: H5] = Set[A] -// def compare[a >: c <: b, b <: d, c <: d, d, A[x >: a <: b] <: B[x], B[_ >: c <: d], x >: a <: b](s: A[x], t: B[_ >: c <: d]) = null + // X2 bounds [H5, H5] are NOT within X bounds [H4, H2] because H5 <: H4 is false + assertNotChild(`LTT[A,B,_>:B<:A]`[H5, H5, X2], `LTT[A,B,_>:B<:A]`[H2, H4, X]) - brokenOnScala3 { -// compare[H5, H5, H4, H2, X2, X, H5](null: Set[H5], null) // error -// (null: Set[H5]): Set[_ >: H4 <: H2] // error - assertNotChild(`LTT[A,B,_>:B<:A]`[H5, H5, X2], `LTT[A,B,_>:B<:A]`[H2, H4, X]) - } - -// compare[H3, H3, H4, H2, X1, X, H3](null: Set[H3], null) -// (null: Set[H3]): Set[_ >: H4 <: H2] - brokenOnScala2 { - assertChild(`LTT[A,B,_>:B<:A]`[H3, H3, X1], `LTT[A,B,_>:B<:A]`[H2, H4, X]) - } + // X1 bounds [H3, H3] ARE within X bounds [H4, H2] because H4 <: H3 and H3 <: H2 + assertChild(`LTT[A,B,_>:B<:A]`[H3, H3, X1], `LTT[A,B,_>:B<:A]`[H2, H4, X]) } "progression test: indirect structural checks do not work" in { From 6dcdca3d116ef519a28f381228804448088e707e Mon Sep 17 00:00:00 2001 From: Pavel Shirshov Date: Tue, 24 Feb 2026 12:13:54 +0000 Subject: [PATCH 3/5] mima checks as a separate job --- .github/workflows/build.yml | 19 ++++++++++++++++++- .mdl/defs/actions.md | 22 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53f7d344..3f7c27ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,6 +52,23 @@ jobs: if: needs.checksecret.outputs.HAVE_SECRETS == 'true' uses: scalacenter/sbt-dependency-submission@53544c3e7bf54b0cb7f375f016e5b7446b96b706 + mima: + runs-on: ubuntu-latest + needs: [ 'checksecret' ] + strategy: + fail-fast: false + matrix: + java: [ '17' ] + scala: [ '2.11', '2.12', '2.13', '3' ] + steps: + - uses: 7mind/github-env@minimal + - name: MIMA Binary Compatibility Checks + env: + CI_PULL_REQUEST: ${{ github.event_name == 'pull_request' }} + CI_BRANCH: ${{ github.ref_name }} + CI_BRANCH_TAG: ${{ github.ref_name }} + run: nix develop --command mdl -u java_version:${{ matrix.java }} -u scala_version:${{ matrix.scala }} --github-actions :gen :mima + publish-artifacts: runs-on: ubuntu-latest needs: [ 'build', 'checksecret' ] @@ -119,7 +136,7 @@ jobs: all-good: if: always() runs-on: ubuntu-latest - needs: [ 'build' ] + needs: [ 'build', 'mima' ] steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 diff --git a/.mdl/defs/actions.md b/.mdl/defs/actions.md index 2143366e..7c283eda 100644 --- a/.mdl/defs/actions.md +++ b/.mdl/defs/actions.md @@ -178,7 +178,7 @@ bash sbtgen.sc --js --native # action: test -Run tests and binary compatibility checks +Run tests ```bash # Declare dependencies and use their outputs @@ -194,7 +194,25 @@ sbt -batch -no-colors -v \ --java-home "$JAVA_HOME" \ "$VERSION_COMMAND clean" \ "$VERSION_COMMAND Test/compile" \ - "$VERSION_COMMAND test" \ + "$VERSION_COMMAND test" +``` + +# action: mima + +Run binary compatibility checks + +```bash +# Declare dependencies and use their outputs +soft action.gen retain.action.check-sbtgen-staleness + +JAVA_HOME="${action.setup-jdk.java-home}" +PATH="${action.setup-jdk.path}" +JAVA_OPTIONS="${action.setup-jvm-options.java-options}" +_JAVA_OPTIONS="$JAVA_OPTIONS" +VERSION_COMMAND="${action.setup-scala.version-command}" + +sbt -batch -no-colors -v \ + --java-home "$JAVA_HOME" \ "$VERSION_COMMAND mimaReportBinaryIssues" ``` From f9b8095c335bd9ddc67eb32ef7b9a866c4933a9b Mon Sep 17 00:00:00 2001 From: Pavel Shirshov Date: Tue, 24 Feb 2026 12:34:48 +0000 Subject: [PATCH 4/5] mima --- .../izumi/reflect/macrortti/LightTypeTagRef.scala | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 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 fce741c5..0cda7fe0 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 @@ -75,7 +75,8 @@ object LightTypeTagRef extends LTTOrdering { } } - final case class Lambda private[macrortti] (input: List[SymName.LambdaParamName], output: AbstractReference) extends AbstractReference { + // Constructor is public for bincompat (Scala 2 case class companions extend AbstractFunction2). Use Lambda.make for normalized construction. + final case class Lambda(input: List[SymName.LambdaParamName], output: AbstractReference) extends AbstractReference { def normalize(): Lambda = LambdaNorm.normalize(this) override def hashCode(): Int = { @@ -109,12 +110,19 @@ object LightTypeTagRef extends LTTOrdering { unusedParamsSize < paramRefs.size } + @deprecated("bincompat only", "2.3.0") + protected lazy val normalizedParams: List[NameReference] = input.zipWithIndex.map { + case (_, idx) => + NameReference(SymName.LambdaParamName(idx, LightTypeTagRef.LambdaConstants.lambdaFakeParamDepth, inputSize)) + } + lazy val normalizedOutput: AbstractReference = output } - object Lambda { + // bincompat: explicit companion must extend AbstractFunction2 to match Scala 2 case class companion hierarchy + object Lambda extends scala.runtime.AbstractFunction2[List[SymName.LambdaParamName], AbstractReference, Lambda] { def make(input: List[SymName.LambdaParamName], output: AbstractReference): Lambda = { - new Lambda(input, output).normalize() + Lambda(input, output).normalize() } } From d89cd07d415bcabd705ef27be4881dd4f6447530 Mon Sep 17 00:00:00 2001 From: Kai <450507+neko-kai@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:18:09 +0000 Subject: [PATCH 5/5] fix bug which could mangle pre 2.3.0 lambdas on deserialization, deprecate Lambda.apply on Scala 2, make constructor pkg private, make apply pri --- .mdl/defs/actions.md | 1 + flake.lock | 6 ++--- .../bincompat/LambdaObjBincompat.scala | 8 +++++++ .../LambdaObjBincompat.scala | 8 +++++++ .../izumi/reflect/macrortti/LambdaNorm.scala | 2 +- .../reflect/macrortti/LightTypeTag.scala | 11 +++++---- .../macrortti/LightTypeTagInheritance.scala | 19 +++++++-------- .../reflect/macrortti/LightTypeTagRef.scala | 23 +++++++++++++++---- 8 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 izumi-reflect/izumi-reflect/src/main/scala-2.13-/izumi/reflect/internal/bincompat/LambdaObjBincompat.scala create mode 100644 izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/internal.bincompat/LambdaObjBincompat.scala diff --git a/.mdl/defs/actions.md b/.mdl/defs/actions.md index 7c283eda..b55a0946 100644 --- a/.mdl/defs/actions.md +++ b/.mdl/defs/actions.md @@ -213,6 +213,7 @@ VERSION_COMMAND="${action.setup-scala.version-command}" sbt -batch -no-colors -v \ --java-home "$JAVA_HOME" \ + "$VERSION_COMMAND compile" \ "$VERSION_COMMAND mimaReportBinaryIssues" ``` diff --git a/flake.lock b/flake.lock index 073cb44a..b2c9c3b1 100644 --- a/flake.lock +++ b/flake.lock @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1764366776, - "narHash": "sha256-Yz3KCFQ7Ua22OtQ9ILlBW5STEklboXJMUXp1p7rLJrs=", + "lastModified": 1770029348, + "narHash": "sha256-ZVgIpUpzIJWli+r3mQ7uvNAcCRIMEbZpHiBWwHwOeZw=", "owner": "7mind", "repo": "mudyla", - "rev": "aa5dcd64cac3d4924dcd68cb518c6cb7f43bc3d7", + "rev": "5b951a2e237f4989fed925f9def22c169839ed93", "type": "github" }, "original": { diff --git a/izumi-reflect/izumi-reflect/src/main/scala-2.13-/izumi/reflect/internal/bincompat/LambdaObjBincompat.scala b/izumi-reflect/izumi-reflect/src/main/scala-2.13-/izumi/reflect/internal/bincompat/LambdaObjBincompat.scala new file mode 100644 index 00000000..949d2999 --- /dev/null +++ b/izumi-reflect/izumi-reflect/src/main/scala-2.13-/izumi/reflect/internal/bincompat/LambdaObjBincompat.scala @@ -0,0 +1,8 @@ +package izumi.reflect.internal.bincompat + +import izumi.reflect.macrortti.LightTypeTagRef.{AbstractReference, Lambda, SymName} + +private[reflect] abstract class LambdaObjBincompat extends scala.runtime.AbstractFunction2[List[SymName.LambdaParamName], AbstractReference, Lambda] { + @deprecated("Lambda constructor is deprecated, use Lambda.make", "3.1.0") + override def apply(inputs: List[SymName.LambdaParamName], outputs: AbstractReference): Lambda = new Lambda(inputs, outputs) +} diff --git a/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/internal.bincompat/LambdaObjBincompat.scala b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/internal.bincompat/LambdaObjBincompat.scala new file mode 100644 index 00000000..cc85abe8 --- /dev/null +++ b/izumi-reflect/izumi-reflect/src/main/scala-3/izumi/reflect/internal.bincompat/LambdaObjBincompat.scala @@ -0,0 +1,8 @@ +package izumi.reflect.internal.bincompat + +import izumi.reflect.macrortti.LightTypeTagRef.{AbstractReference, Lambda, SymName} + +private[reflect] trait LambdaObjBincompat { + @deprecated("bincompat only", "3.1.0") + private[internal] def apply(inputs: List[SymName.LambdaParamName], outputs: AbstractReference): Lambda = new Lambda(inputs, outputs) +} diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala index 1882b13a..683a822b 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LambdaNorm.scala @@ -103,7 +103,7 @@ private[macrortti] object LambdaNorm { }.toMap val normalizedInput = l.input.map(p => localMap.getOrElse(p, throw new IllegalStateException(s"Missing normalized param for $p"))) val nextEnv = localMap :: env - new Lambda(normalizedInput, normalizeRef(l.output, nextEnv)) + Lambda.unsafeDenormalized(normalizedInput, normalizeRef(l.output, nextEnv)) case IntersectionReference(refs) => IntersectionReference(refs.map(normalizeRef(_, env)).map { case a: AppliedReferenceExceptIntersection => a 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 96a4cae5..18514794 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 @@ -923,15 +923,16 @@ object LightTypeTag { val convertedParams = oldParams.map(SymName.bincompatForceCreateLambdaParamNameFromString(_)) val paramMap = oldParams.iterator.zip(convertedParams.iterator).toMap[String, SymName.LambdaParamName] - val oldLambda = LightTypeTagRef.Lambda.make(convertedParams, lambdaResult) + val oldLambda = LightTypeTagRef.Lambda.unsafeDenormalized(convertedParams, lambdaResult) val value = convertPre230LambdaToNew(oldLambda, paramMap) state.addIdentityRef(value) value } else if (ic == 1) { // Post 2.3.0 - val value = LightTypeTagRef.Lambda.make( - state.unpickle[List[SymName.LambdaParamName]], - state.unpickle[LightTypeTagRef.AbstractReference] - ) + val value = LightTypeTagRef + .Lambda.make( + state.unpickle[List[SymName.LambdaParamName]], + state.unpickle[LightTypeTagRef.AbstractReference] + ) state.addIdentityRef(value) value } else if (ic < 0) diff --git a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala index b48b8736..6d6678de 100644 --- a/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala +++ b/izumi-reflect/izumi-reflect/src/main/scala/izumi/reflect/macrortti/LightTypeTagInheritance.scala @@ -290,7 +290,7 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { prefix.map(p => stripLambdaParamBoundaries(p, params).asInstanceOf[AppliedReference]) ) case Lambda(input, output) => - new Lambda(input, stripLambdaParamBoundaries(output, params)) + Lambda.unsafeDenormalized(input, stripLambdaParamBoundaries(output, params)) case IntersectionReference(refs) => IntersectionReference(refs.map(r => stripLambdaParamBoundaries(r, params).asInstanceOf[AppliedReferenceExceptIntersection])) case UnionReference(refs) => @@ -567,14 +567,15 @@ final class LightTypeTagInheritance(self: LightTypeTag, other: LightTypeTag) { } private def resolveTemplateParameterizedParents(actual: FullReference): Set[AbstractReference] = { - basesdb.iterator.collect { - case (template: FullReference, templateParents) => - buildTemplateSubstitution(template, actual).map { - substitution => - val rewriter = new RuntimeAPI.Rewriter(substitution) - templateParents.map(rewriter.replaceRefs) - } - }.flatten.flatten.toSet + basesdb + .iterator.collect { + case (template: FullReference, templateParents) => + buildTemplateSubstitution(template, actual).map { + substitution => + val rewriter = new RuntimeAPI.Rewriter(substitution) + templateParents.map(rewriter.replaceRefs) + } + }.flatten.flatten.toSet } private def buildTemplateSubstitution( 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 0cda7fe0..f8dfcb08 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 @@ -18,8 +18,10 @@ package izumi.reflect.macrortti +import izumi.reflect.internal.NowarnCompat +import izumi.reflect.internal.bincompat.LambdaObjBincompat import izumi.reflect.macrortti.LightTypeTagRef.{AbstractReference, AppliedReference} -import izumi.reflect.macrortti.LightTypeTagRef.SymName.{LambdaParamName, SymTypeName} +import izumi.reflect.macrortti.LightTypeTagRef.SymName.SymTypeName import scala.runtime.AbstractFunction3 import scala.util.hashing.MurmurHash3 @@ -76,8 +78,14 @@ object LightTypeTagRef extends LTTOrdering { } // Constructor is public for bincompat (Scala 2 case class companions extend AbstractFunction2). Use Lambda.make for normalized construction. - final case class Lambda(input: List[SymName.LambdaParamName], output: AbstractReference) extends AbstractReference { - def normalize(): Lambda = LambdaNorm.normalize(this) + final case class Lambda @deprecated("Lambda constructor is deprecated, use Lambda.make", "3.1.0") private[reflect] ( + input: List[SymName.LambdaParamName], + output: AbstractReference + ) extends AbstractReference { + + def normalize(): Lambda = { + LambdaNorm.normalize(this) + } override def hashCode(): Int = { inputSize * 31 + output.hashCode() @@ -120,9 +128,14 @@ object LightTypeTagRef extends LTTOrdering { } // bincompat: explicit companion must extend AbstractFunction2 to match Scala 2 case class companion hierarchy - object Lambda extends scala.runtime.AbstractFunction2[List[SymName.LambdaParamName], AbstractReference, Lambda] { + @NowarnCompat.nowarn("msg=deprecated") + object Lambda extends LambdaObjBincompat { def make(input: List[SymName.LambdaParamName], output: AbstractReference): Lambda = { - Lambda(input, output).normalize() + new Lambda(input, output).normalize() + } + + def unsafeDenormalized(input: List[SymName.LambdaParamName], output: AbstractReference): Lambda = { + new Lambda(input, output) } }