From 94e1ccc84f2fd7ed9ab50e84b3595615ccb273d9 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 2 Dec 2025 14:14:05 +0100 Subject: [PATCH 1/8] Don't treat SAM class with Unit result type as a platform SAM --- .../tools/dotc/config/JavaPlatform.scala | 5 +++- tests/run/i24573.check | 5 ++++ tests/run/i24573.scala | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/run/i24573.check create mode 100644 tests/run/i24573.scala diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 80cb9f556867..8080912d3f24 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -50,7 +50,10 @@ class JavaPlatform extends Platform { cls.superClass == defn.ObjectClass && cls.directlyInheritedTraits.forall(_.is(NoInits)) && !ExplicitOuter.needsOuterIfReferenced(cls) && - cls.typeRef.fields.isEmpty // Superaccessors already show up as abstract methods here, so no test necessary + // Superaccessors already show up as abstract methods here, so no test necessary + cls.typeRef.fields.isEmpty && + // LambdaMetafactory can't handle SAMs with Unit return type unless it's a FunctionN itself + !cls.typeRef.possibleSamMethods.exists(_.info.resultType.isRef(defn.UnitClass)) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal diff --git a/tests/run/i24573.check b/tests/run/i24573.check new file mode 100644 index 000000000000..e500620c36e6 --- /dev/null +++ b/tests/run/i24573.check @@ -0,0 +1,5 @@ +1 +2 +3 +42 +hello diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala new file mode 100644 index 000000000000..5c2dd14c6a95 --- /dev/null +++ b/tests/run/i24573.scala @@ -0,0 +1,25 @@ +trait Con[-T] extends (T => Unit): + def apply(t: T): Unit + +trait Con2[-T] extends (T => Int): + def apply(t: T): Int + +trait Con3[+R] extends (() => R): + def apply(): R + +object Test: + def main(args: Array[String]): Unit = + val f1: (Int => Unit) = i => println(i) + f1(1) + + val c1: Con[Int] = i => println(i) + c1(2) + + val c2: Con2[Int] = i => { println(i); i } + c2(3) + + val c3: Con3[Int] = () => 42 + println(c3()) + + val c4: Con3[Unit] = () => println("hello") + c4() From 46ae660b51333701187da17fd8a0556f872a03ed Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 2 Dec 2025 14:24:59 +0100 Subject: [PATCH 2/8] Update the test to include a custom function1 type --- tests/run/i24573.check | 1 + tests/run/i24573.scala | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/tests/run/i24573.check b/tests/run/i24573.check index e500620c36e6..7dbcbc314ebb 100644 --- a/tests/run/i24573.check +++ b/tests/run/i24573.check @@ -3,3 +3,4 @@ 3 42 hello +world diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala index 5c2dd14c6a95..4efb850ab578 100644 --- a/tests/run/i24573.scala +++ b/tests/run/i24573.scala @@ -7,6 +7,12 @@ trait Con2[-T] extends (T => Int): trait Con3[+R] extends (() => R): def apply(): R +trait F1[-T, +R] { + def apply(t: T): R +} + +trait SF[-T] extends F1[T, Unit] { def apply(t: T): Unit } + object Test: def main(args: Array[String]): Unit = val f1: (Int => Unit) = i => println(i) @@ -23,3 +29,6 @@ object Test: val c4: Con3[Unit] = () => println("hello") c4() + + val f5: SF[String] = s => println(s) + f5("world") From 35a8c53e73568af6e8aeba6db21c82f61620ddf5 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Tue, 2 Dec 2025 16:12:58 +0100 Subject: [PATCH 3/8] Check erased SAM method signature compatibility --- .../dotty/tools/dotc/config/JavaPlatform.scala | 18 ++++++++++++++++-- tests/run/i24573.check | 1 + tests/run/i24573.scala | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 8080912d3f24..92f234aa2cc7 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -6,6 +6,7 @@ import io.* import classpath.AggregateClassPath import core.* import Symbols.*, Types.*, Contexts.*, StdNames.* +import Phases.* import Flags.* import transform.ExplicitOuter @@ -44,6 +45,17 @@ class JavaPlatform extends Platform { def rootLoader(root: TermSymbol)(using Context): SymbolLoader = new SymbolLoaders.PackageLoader(root, classPath) + private def samMethodHasCompatibleErasedSignature(cls: ClassSymbol)(using Context): Boolean = atPhase(erasurePhase): + cls.typeRef.possibleSamMethods.toList match + case samDenot :: Nil => + val samMethod = samDenot.symbol + val samErasedResult = TypeErasure.erasure(samMethod.info.resultType) + samMethod.allOverriddenSymbols.forall { overridden => + val overriddenErasedResult = TypeErasure.erasure(overridden.info.resultType) + samErasedResult =:= overriddenErasedResult + } + case _ => false // No SAM method or multiple - handled elsewhere + /** Is the SAMType `cls` also a SAM under the rules of the JVM? */ def isSam(cls: ClassSymbol)(using Context): Boolean = cls.isAllOf(NoInitsTrait) && @@ -52,8 +64,10 @@ class JavaPlatform extends Platform { !ExplicitOuter.needsOuterIfReferenced(cls) && // Superaccessors already show up as abstract methods here, so no test necessary cls.typeRef.fields.isEmpty && - // LambdaMetafactory can't handle SAMs with Unit return type unless it's a FunctionN itself - !cls.typeRef.possibleSamMethods.exists(_.info.resultType.isRef(defn.UnitClass)) + // Check that SAM method's erased signature is compatible with all overridden methods + // For example, `void apply(Object)` is not compatible with `Object apply(Object)` + // even though both can have the same type signature `def apply(o: Object): Unit` before erasure. + samMethodHasCompatibleErasedSignature(cls) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal diff --git a/tests/run/i24573.check b/tests/run/i24573.check index 7dbcbc314ebb..67b3b77496a5 100644 --- a/tests/run/i24573.check +++ b/tests/run/i24573.check @@ -4,3 +4,4 @@ 42 hello world +!! diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala index 4efb850ab578..944e41d30495 100644 --- a/tests/run/i24573.scala +++ b/tests/run/i24573.scala @@ -7,11 +7,17 @@ trait Con2[-T] extends (T => Int): trait Con3[+R] extends (() => R): def apply(): R -trait F1[-T, +R] { +trait F1[-T, +R]: def apply(t: T): R -} -trait SF[-T] extends F1[T, Unit] { def apply(t: T): Unit } +trait SF[-T] extends F1[T, Unit]: + def apply(t: T): Unit + +trait F1U[-T]: + def apply(t: T): Unit + +trait SF2 extends F1U[String]: + def apply(t: String): Unit object Test: def main(args: Array[String]): Unit = @@ -32,3 +38,6 @@ object Test: val f5: SF[String] = s => println(s) f5("world") + + val f6: SF2 = i => println(i) + f6("!!") From 829865bac12ffc60594f099c89ea21048a14948d Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Wed, 3 Dec 2025 03:11:45 +0100 Subject: [PATCH 4/8] Refactor platform SAM check to ensure compatible bridge for LambdaMetaFactory --- .../tools/dotc/config/JavaPlatform.scala | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 92f234aa2cc7..6ae24347f9ec 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -45,16 +45,18 @@ class JavaPlatform extends Platform { def rootLoader(root: TermSymbol)(using Context): SymbolLoader = new SymbolLoaders.PackageLoader(root, classPath) - private def samMethodHasCompatibleErasedSignature(cls: ClassSymbol)(using Context): Boolean = atPhase(erasurePhase): - cls.typeRef.possibleSamMethods.toList match - case samDenot :: Nil => - val samMethod = samDenot.symbol - val samErasedResult = TypeErasure.erasure(samMethod.info.resultType) - samMethod.allOverriddenSymbols.forall { overridden => - val overriddenErasedResult = TypeErasure.erasure(overridden.info.resultType) - samErasedResult =:= overriddenErasedResult - } - case _ => false // No SAM method or multiple - handled elsewhere + private def samMethodHasCompatibleBridge(cls: ClassSymbol)(using Context): Boolean = + cls.typeRef.possibleSamMethods match + case Seq(samMeth) => + val samResultType = samMeth.info.resultType + if samResultType.isRef(defn.UnitClass) then + // If the result type of the SAM method is Unit, but the result type of the overridden + // methods is not Unit, the bridge will return Object, which is not compatible with Void + // required by LambdaMetaFactory. + // See issue #24573 for details. + samMeth.symbol.allOverriddenSymbols.forall(_.info.resultType.isRef(defn.UnitClass)) + else true + case _ => false /** Is the SAMType `cls` also a SAM under the rules of the JVM? */ def isSam(cls: ClassSymbol)(using Context): Boolean = @@ -64,10 +66,8 @@ class JavaPlatform extends Platform { !ExplicitOuter.needsOuterIfReferenced(cls) && // Superaccessors already show up as abstract methods here, so no test necessary cls.typeRef.fields.isEmpty && - // Check that SAM method's erased signature is compatible with all overridden methods - // For example, `void apply(Object)` is not compatible with `Object apply(Object)` - // even though both can have the same type signature `def apply(o: Object): Unit` before erasure. - samMethodHasCompatibleErasedSignature(cls) + // Check if SAM method will have a compatible bridge for LambdaMetaFactory + samMethodHasCompatibleBridge(cls) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal From b46a29f1c7cfa92f8078b71088aa723610b1a59d Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 4 Dec 2025 17:42:31 +0100 Subject: [PATCH 5/8] Refactor SAM method compatibility checks using similar logic in Erasure.Boxing.adaptClosure --- .../tools/dotc/config/JavaPlatform.scala | 17 +- .../dotty/tools/dotc/core/TypeErasure.scala | 76 ++++++++- tests/run/i24573.check | 34 +++- tests/run/i24573.scala | 148 +++++++++++++++--- 4 files changed, 233 insertions(+), 42 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 6ae24347f9ec..6b10888f4d20 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -45,19 +45,6 @@ class JavaPlatform extends Platform { def rootLoader(root: TermSymbol)(using Context): SymbolLoader = new SymbolLoaders.PackageLoader(root, classPath) - private def samMethodHasCompatibleBridge(cls: ClassSymbol)(using Context): Boolean = - cls.typeRef.possibleSamMethods match - case Seq(samMeth) => - val samResultType = samMeth.info.resultType - if samResultType.isRef(defn.UnitClass) then - // If the result type of the SAM method is Unit, but the result type of the overridden - // methods is not Unit, the bridge will return Object, which is not compatible with Void - // required by LambdaMetaFactory. - // See issue #24573 for details. - samMeth.symbol.allOverriddenSymbols.forall(_.info.resultType.isRef(defn.UnitClass)) - else true - case _ => false - /** Is the SAMType `cls` also a SAM under the rules of the JVM? */ def isSam(cls: ClassSymbol)(using Context): Boolean = cls.isAllOf(NoInitsTrait) && @@ -66,8 +53,8 @@ class JavaPlatform extends Platform { !ExplicitOuter.needsOuterIfReferenced(cls) && // Superaccessors already show up as abstract methods here, so no test necessary cls.typeRef.fields.isEmpty && - // Check if SAM method will have a compatible bridge for LambdaMetaFactory - samMethodHasCompatibleBridge(cls) + // Check if the SAM can be implemented via LambdaMetaFactory + !TypeErasure.samNeedsExpansion(cls) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index c29b971f1a5a..9dc553a121e2 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -74,7 +74,7 @@ end SourceLanguage * only for isInstanceOf, asInstanceOf: PolyType, TypeParamRef, TypeBounds * */ -object TypeErasure { +object TypeErasure: private def erasureDependsOnArgs(sym: Symbol)(using Context) = sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass @@ -586,7 +586,79 @@ object TypeErasure { defn.FunctionType(n = info.nonErasedParamCount) } erasure(functionType(applyInfo)) -} + + /** Check if LambdaMetaFactory cannot handle the SAM method's required signature adaptation. + * + * When a SAM method overrides other methods, the erased signatures must be compatible + * for LambdaMetaFactory to work. This method returns true if any overridden method + * has an incompatible erased signature that LMF cannot auto-adapt. + * + * The adaptation rules mirror those in `Erasure.Boxing.adaptClosure`: + * - For parameters: primitives and value classes cannot be auto-adapted by LMF + * - For results: value classes and Unit cannot be auto-adapted by LMF + * + * When this returns true, the SAM class must be expanded rather than using LMF. + * + * @param cls The SAM class to check + * @return true if LMF cannot handle the required adaptation + */ + def samNeedsExpansion(cls: ClassSymbol)(using Context): Boolean = + val Seq(samMeth) = cls.typeRef.possibleSamMethods + val samMethSym = samMeth.symbol + val erasedSamInfo = transformInfo(samMethSym, samMeth.info) + // println(i"Checking whether SAM ${cls} needs expansion, erased SAM info: $erasedSamInfo") + + val (erasedSamParamTypes, erasedSamResultType) = erasedSamInfo match + case mt: MethodType => (mt.paramInfos, mt.resultType) + case _ => return false + + def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol + + /** Can the implementation parameter type `tp` be auto-adapted to a different + * parameter type in the SAM? + * + * For derived value classes, we always need to do the bridging manually. + * For primitives, we cannot rely on auto-adaptation on the JVM because + * the Scala spec requires null to be "unboxed" to the default value of + * the value class, but the adaptation performed by LambdaMetaFactory + * will throw a `NullPointerException` instead. + */ + def autoAdaptedParam(tp: Type) = + !tp.isErasedValueType && !tp.isPrimitiveValueType + + /** Can the implementation result type be auto-adapted to a different result + * type in the SAM? + * + * For derived value classes, it's the same story as for parameters. + * For non-Unit primitives, we can actually rely on the `LambdaMetaFactory` + * adaptation, because it only needs to box, not unbox, so no special + * handling of null is required. + */ + def autoAdaptedResult(implResultType: Type) = + !implResultType.isErasedValueType && !(implResultType.classSymbol eq defn.UnitClass) + + samMethSym.allOverriddenSymbols.exists { overridden => + val erasedOverriddenInfo = transformInfo(overridden, overridden.info) + // println(i" comparing to overridden method ${overridden} with erased info: $erasedOverriddenInfo") + erasedOverriddenInfo match + case mt: MethodType => + val overriddenParamTypes = mt.paramInfos + val overriddenResultType = mt.resultType + + val paramAdaptationNeeded = + erasedSamParamTypes.lazyZip(overriddenParamTypes).exists((samType, overriddenType) => + !sameClass(samType, overriddenType) && (!autoAdaptedParam(samType) + // LambdaMetaFactory cannot auto-adapt between Object and Array types + || overriddenType.isInstanceOf[JavaArrayType])) + + val resultAdaptationNeeded = + !sameClass(erasedSamResultType, overriddenResultType) && !autoAdaptedResult(erasedSamResultType) + + paramAdaptationNeeded || resultAdaptationNeeded + case _ => false + } + end samNeedsExpansion +end TypeErasure import TypeErasure.* diff --git a/tests/run/i24573.check b/tests/run/i24573.check index 67b3b77496a5..aba1c959f350 100644 --- a/tests/run/i24573.check +++ b/tests/run/i24573.check @@ -1,7 +1,35 @@ 1 2 3 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +31 +32 +41 42 -hello -world -!! +43 +44 +45 +46 +51 +52 +53 +55 +56 +57 +61 +62 +63 +64 diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala index 944e41d30495..9a4ff6bce3ce 100644 --- a/tests/run/i24573.scala +++ b/tests/run/i24573.scala @@ -1,43 +1,147 @@ -trait Con[-T] extends (T => Unit): +trait ConTU[-T] extends (T => Unit): def apply(t: T): Unit -trait Con2[-T] extends (T => Int): +trait ConTI[-T] extends (T => Int): def apply(t: T): Int -trait Con3[+R] extends (() => R): +trait ConTS[-T] extends (T => String): + def apply(t: T): String + +trait ConIR[+R] extends (Int => R): + def apply(t: Int): R + +trait ConSR[+R] extends (String => R): + def apply(t: String): R + +trait ConUR[+R] extends (() => R): def apply(): R +trait ConII extends (Int => Int): + def apply(t: Int): Int + +trait ConSI extends (String => Int): + def apply(t: String): Int + +trait ConIS extends (Int => String): + def apply(t: Int): String + +trait ConUU extends (() => Unit): + def apply(): Unit + trait F1[-T, +R]: def apply(t: T): R -trait SF[-T] extends F1[T, Unit]: +trait SFTU[-T] extends F1[T, Unit]: def apply(t: T): Unit -trait F1U[-T]: - def apply(t: T): Unit +trait SFTI[-T] extends F1[T, Int]: + def apply(t: T): Int + +trait SFTS[-T] extends F1[T, String]: + def apply(t: T): String + +trait SFIR [+R] extends F1[Int, R]: + def apply(t: Int): R + +trait SFSR [+R] extends F1[String, R]: + def apply(t: String): R + +trait SFII extends F1[Int, Int]: + def apply(t: Int): Int + +trait SFSI extends F1[String, Int]: + def apply(t: String): Int + +trait SFIS extends F1[Int, String]: + def apply(t: Int): String + +trait SFIU extends F1[Int, Unit]: + def apply(t: Int): Unit -trait SF2 extends F1U[String]: - def apply(t: String): Unit object Test: def main(args: Array[String]): Unit = - val f1: (Int => Unit) = i => println(i) - f1(1) + val fIU: (Int => Unit) = (x: Int) => println(x) + fIU(1) + + val fIS: (Int => String) = (x: Int) => x.toString + println(fIS(2)) + + val fUI: (() => Int) = () => 3 + println(fUI()) + + val conITU: ConTU[Int] = (x: Int) => println(x) + conITU(11) + val conITI: ConTI[Int] = (x: Int) => x + println(conITI(12)) + val conITS: ConTS[Int] = (x: Int) => x.toString + println(conITS(13)) + val conSTS: ConTS[String] = (x: String) => x + println(conSTS("14")) + + val conIRS: ConIR[String] = (x: Int) => x.toString + println(conIRS(15)) + val conIRI: ConIR[Int] = (x: Int) => x + println(conIRI(16)) + val conIRU: ConIR[Unit] = (x: Int) => println(x) + conIRU(17) + + val conSRI: ConSR[Int] = (x: String) => x.toInt + println(conSRI("18")) + val conURI: ConUR[Int] = () => 19 + println(conURI()) + val conURU: ConUR[Unit] = () => println("20") + conURU() + + val conII: ConII = (x: Int) => x + println(conII(21)) + val conSI: ConSI = (x: String) => x.toInt + println(conSI("22")) + val conIS: ConIS = (x: Int) => x.toString + println(conIS(23)) + val conUU: ConUU = () => println("24") + conUU() + + val ffIU: F1[Int, Unit] = (x: Int) => println(x) + ffIU(31) + val ffIS: F1[Int, String] = (x: Int) => x.toString + println(ffIS(32)) + + val sfITU: SFTU[Int] = (x: Int) => println(x) + sfITU(41) + val sfSTU: SFTU[String] = (x: String) => println(x) + sfSTU("42") - val c1: Con[Int] = i => println(i) - c1(2) + val sfITI: SFTI[Int] = (x: Int) => x + println(sfITI(43)) + val sfSTI: SFTI[String] = (x: String) => x.toInt + println(sfSTI("44")) - val c2: Con2[Int] = i => { println(i); i } - c2(3) + val sfITS: SFTS[Int] = (x: Int) => x.toString + println(sfITS(45)) + val sfSTS: SFTS[String] = (x: String) => x + println(sfSTS("46")) - val c3: Con3[Int] = () => 42 - println(c3()) + val sfIRI: SFIR[Int] = (x: Int) => x + println(sfIRI(51)) + val sfIRS: SFIR[String] = (x: Int) => x.toString + println(sfIRS(52)) + val sfIRU: SFIR[Unit] = (x: Int) => println(x) + sfIRU(53) - val c4: Con3[Unit] = () => println("hello") - c4() + val sfSRI: SFSR[Int] = (x: String) => x.toInt + println(sfSRI("55")) + val sfSRS: SFSR[String] = (x: String) => x + println(sfSRS("56")) + val sfSRU: SFSR[Unit] = (x: String) => println(x) + sfSRU("57") - val f5: SF[String] = s => println(s) - f5("world") + val sfII: SFII = (x: Int) => x + println(sfII(61)) + val sfSI: SFSI = (x: String) => x.toInt + println(sfSI("62")) + val sfIS: SFIS = (x: Int) => x.toString + println(sfIS(63)) + val sfIU: SFIU = (x: Int) => println(x) + sfIU(64) - val f6: SF2 = i => println(i) - f6("!!") From d914da59fde1e8b53409748959fd922ac2b303de Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 4 Dec 2025 17:54:13 +0100 Subject: [PATCH 6/8] Add comments to the test --- tests/run/i24573.check | 2 ++ tests/run/i24573.scala | 74 ++++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/tests/run/i24573.check b/tests/run/i24573.check index aba1c959f350..65cf09ede65d 100644 --- a/tests/run/i24573.check +++ b/tests/run/i24573.check @@ -17,6 +17,8 @@ 24 31 32 +33 +34 41 42 43 diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala index 9a4ff6bce3ce..a2f1e7525e15 100644 --- a/tests/run/i24573.scala +++ b/tests/run/i24573.scala @@ -61,87 +61,91 @@ trait SFIU extends F1[Int, Unit]: object Test: def main(args: Array[String]): Unit = - val fIU: (Int => Unit) = (x: Int) => println(x) + val fIU: (Int => Unit) = (x: Int) => println(x) // closure by JFunction1 fIU(1) - val fIS: (Int => String) = (x: Int) => x.toString + val fIS: (Int => String) = (x: Int) => x.toString // closure println(fIS(2)) - val fUI: (() => Int) = () => 3 + val fUI: (() => Int) = () => 3 // closure println(fUI()) - val conITU: ConTU[Int] = (x: Int) => println(x) + val conITU: ConTU[Int] = (x: Int) => println(x) // expanded conITU(11) - val conITI: ConTI[Int] = (x: Int) => x + val conITI: ConTI[Int] = (x: Int) => x // closure println(conITI(12)) - val conITS: ConTS[Int] = (x: Int) => x.toString + val conITS: ConTS[Int] = (x: Int) => x.toString // closure println(conITS(13)) - val conSTS: ConTS[String] = (x: String) => x + val conSTS: ConTS[String] = (x: String) => x // closure println(conSTS("14")) - val conIRS: ConIR[String] = (x: Int) => x.toString + val conIRS: ConIR[String] = (x: Int) => x.toString // expanded println(conIRS(15)) - val conIRI: ConIR[Int] = (x: Int) => x + val conIRI: ConIR[Int] = (x: Int) => x // expanded println(conIRI(16)) - val conIRU: ConIR[Unit] = (x: Int) => println(x) + val conIRU: ConIR[Unit] = (x: Int) => println(x) // expanded conIRU(17) - val conSRI: ConSR[Int] = (x: String) => x.toInt + val conSRI: ConSR[Int] = (x: String) => x.toInt // closure println(conSRI("18")) - val conURI: ConUR[Int] = () => 19 + val conURI: ConUR[Int] = () => 19 // closure println(conURI()) - val conURU: ConUR[Unit] = () => println("20") + val conURU: ConUR[Unit] = () => println("20") // closure conURU() - val conII: ConII = (x: Int) => x + val conII: ConII = (x: Int) => x // expanded println(conII(21)) - val conSI: ConSI = (x: String) => x.toInt + val conSI: ConSI = (x: String) => x.toInt // closure println(conSI("22")) - val conIS: ConIS = (x: Int) => x.toString + val conIS: ConIS = (x: Int) => x.toString // expanded println(conIS(23)) - val conUU: ConUU = () => println("24") + val conUU: ConUU = () => println("24") // expanded conUU() - val ffIU: F1[Int, Unit] = (x: Int) => println(x) + val ffIU: F1[Int, Unit] = (x: Int) => println(x) // closure ffIU(31) - val ffIS: F1[Int, String] = (x: Int) => x.toString + val ffIS: F1[Int, String] = (x: Int) => x.toString // closure println(ffIS(32)) + val ffSU: F1[String, Unit] = (x: String) => println(x) // closure + ffSU("33") + val ffSI: F1[String, Int] = (x: String) => x.toInt // closure + println(ffSI("34")) - val sfITU: SFTU[Int] = (x: Int) => println(x) + val sfITU: SFTU[Int] = (x: Int) => println(x) // expanded sfITU(41) - val sfSTU: SFTU[String] = (x: String) => println(x) + val sfSTU: SFTU[String] = (x: String) => println(x) // expanded sfSTU("42") - val sfITI: SFTI[Int] = (x: Int) => x + val sfITI: SFTI[Int] = (x: Int) => x // closure println(sfITI(43)) - val sfSTI: SFTI[String] = (x: String) => x.toInt + val sfSTI: SFTI[String] = (x: String) => x.toInt // closure println(sfSTI("44")) - val sfITS: SFTS[Int] = (x: Int) => x.toString + val sfITS: SFTS[Int] = (x: Int) => x.toString // closure println(sfITS(45)) - val sfSTS: SFTS[String] = (x: String) => x + val sfSTS: SFTS[String] = (x: String) => x // closure println(sfSTS("46")) - val sfIRI: SFIR[Int] = (x: Int) => x + val sfIRI: SFIR[Int] = (x: Int) => x // expanded println(sfIRI(51)) - val sfIRS: SFIR[String] = (x: Int) => x.toString + val sfIRS: SFIR[String] = (x: Int) => x.toString // expanded println(sfIRS(52)) - val sfIRU: SFIR[Unit] = (x: Int) => println(x) + val sfIRU: SFIR[Unit] = (x: Int) => println(x) // expanded sfIRU(53) - val sfSRI: SFSR[Int] = (x: String) => x.toInt + val sfSRI: SFSR[Int] = (x: String) => x.toInt // closure println(sfSRI("55")) - val sfSRS: SFSR[String] = (x: String) => x + val sfSRS: SFSR[String] = (x: String) => x // closure println(sfSRS("56")) - val sfSRU: SFSR[Unit] = (x: String) => println(x) + val sfSRU: SFSR[Unit] = (x: String) => println(x) // closure sfSRU("57") - val sfII: SFII = (x: Int) => x + val sfII: SFII = (x: Int) => x // expanded println(sfII(61)) - val sfSI: SFSI = (x: String) => x.toInt + val sfSI: SFSI = (x: String) => x.toInt // closure println(sfSI("62")) - val sfIS: SFIS = (x: Int) => x.toString + val sfIS: SFIS = (x: Int) => x.toString // expanded println(sfIS(63)) - val sfIU: SFIU = (x: Int) => println(x) + val sfIU: SFIU = (x: Int) => println(x) // expanded sfIU(64) From dffabb2c7ce954a5ddf06a440f5b5557529a7573 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 4 Dec 2025 22:09:24 +0100 Subject: [PATCH 7/8] Handle classes which have multiple possible sam functions --- .../tools/dotc/config/JavaPlatform.scala | 2 +- .../dotty/tools/dotc/core/TypeErasure.scala | 116 +++++++++--------- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala index 6b10888f4d20..23a589003f4a 100644 --- a/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala +++ b/compiler/src/dotty/tools/dotc/config/JavaPlatform.scala @@ -54,7 +54,7 @@ class JavaPlatform extends Platform { // Superaccessors already show up as abstract methods here, so no test necessary cls.typeRef.fields.isEmpty && // Check if the SAM can be implemented via LambdaMetaFactory - !TypeErasure.samNeedsExpansion(cls) + TypeErasure.samDoesNotNeedExpansion(cls) /** We could get away with excluding BoxedBooleanClass for the * purpose of equality testing since it need not compare equal diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index 9dc553a121e2..86c2eaf12d73 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -587,7 +587,7 @@ object TypeErasure: } erasure(functionType(applyInfo)) - /** Check if LambdaMetaFactory cannot handle the SAM method's required signature adaptation. + /** Check if LambdaMetaFactory can handle the SAM method's required signature adaptation. * * When a SAM method overrides other methods, the erased signatures must be compatible * for LambdaMetaFactory to work. This method returns true if any overridden method @@ -597,67 +597,65 @@ object TypeErasure: * - For parameters: primitives and value classes cannot be auto-adapted by LMF * - For results: value classes and Unit cannot be auto-adapted by LMF * - * When this returns true, the SAM class must be expanded rather than using LMF. + * When this returns true, the SAM class does not need to be expanded. * * @param cls The SAM class to check - * @return true if LMF cannot handle the required adaptation + * @return true if LMF can handle the required adaptation */ - def samNeedsExpansion(cls: ClassSymbol)(using Context): Boolean = - val Seq(samMeth) = cls.typeRef.possibleSamMethods - val samMethSym = samMeth.symbol - val erasedSamInfo = transformInfo(samMethSym, samMeth.info) - // println(i"Checking whether SAM ${cls} needs expansion, erased SAM info: $erasedSamInfo") - - val (erasedSamParamTypes, erasedSamResultType) = erasedSamInfo match - case mt: MethodType => (mt.paramInfos, mt.resultType) - case _ => return false - - def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol - - /** Can the implementation parameter type `tp` be auto-adapted to a different - * parameter type in the SAM? - * - * For derived value classes, we always need to do the bridging manually. - * For primitives, we cannot rely on auto-adaptation on the JVM because - * the Scala spec requires null to be "unboxed" to the default value of - * the value class, but the adaptation performed by LambdaMetaFactory - * will throw a `NullPointerException` instead. - */ - def autoAdaptedParam(tp: Type) = - !tp.isErasedValueType && !tp.isPrimitiveValueType - - /** Can the implementation result type be auto-adapted to a different result - * type in the SAM? - * - * For derived value classes, it's the same story as for parameters. - * For non-Unit primitives, we can actually rely on the `LambdaMetaFactory` - * adaptation, because it only needs to box, not unbox, so no special - * handling of null is required. - */ - def autoAdaptedResult(implResultType: Type) = - !implResultType.isErasedValueType && !(implResultType.classSymbol eq defn.UnitClass) - - samMethSym.allOverriddenSymbols.exists { overridden => - val erasedOverriddenInfo = transformInfo(overridden, overridden.info) - // println(i" comparing to overridden method ${overridden} with erased info: $erasedOverriddenInfo") - erasedOverriddenInfo match - case mt: MethodType => - val overriddenParamTypes = mt.paramInfos - val overriddenResultType = mt.resultType - - val paramAdaptationNeeded = - erasedSamParamTypes.lazyZip(overriddenParamTypes).exists((samType, overriddenType) => - !sameClass(samType, overriddenType) && (!autoAdaptedParam(samType) - // LambdaMetaFactory cannot auto-adapt between Object and Array types - || overriddenType.isInstanceOf[JavaArrayType])) - - val resultAdaptationNeeded = - !sameClass(erasedSamResultType, overriddenResultType) && !autoAdaptedResult(erasedSamResultType) - - paramAdaptationNeeded || resultAdaptationNeeded - case _ => false - } - end samNeedsExpansion + def samDoesNotNeedExpansion(cls: ClassSymbol)(using Context): Boolean = cls.typeRef.possibleSamMethods match + case Seq(samMeth) => + val samMethSym = samMeth.symbol + val erasedSamInfo = transformInfo(samMethSym, samMeth.info) + + val (erasedSamParamTypes, erasedSamResultType) = erasedSamInfo match + case mt: MethodType => (mt.paramInfos, mt.resultType) + case _ => return false + + def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol + + /** Can the implementation parameter type `tp` be auto-adapted to a different + * parameter type in the SAM? + * + * For derived value classes, we always need to do the bridging manually. + * For primitives, we cannot rely on auto-adaptation on the JVM because + * the Scala spec requires null to be "unboxed" to the default value of + * the value class, but the adaptation performed by LambdaMetaFactory + * will throw a `NullPointerException` instead. + */ + def autoAdaptedParam(tp: Type) = !tp.isErasedValueType && !tp.isPrimitiveValueType + + /** Can the implementation result type be auto-adapted to a different result + * type in the SAM? + * + * For derived value classes, it's the same story as for parameters. + * For non-Unit primitives, we can actually rely on the `LambdaMetaFactory` + * adaptation, because it only needs to box, not unbox, so no special + * handling of null is required. + */ + def autoAdaptedResult(implResultType: Type) = + !implResultType.isErasedValueType && !(implResultType.classSymbol eq defn.UnitClass) + + samMethSym.allOverriddenSymbols.forall { overridden => + val erasedOverriddenInfo = transformInfo(overridden, overridden.info) + erasedOverriddenInfo match + case mt: MethodType => + val overriddenParamTypes = mt.paramInfos + val overriddenResultType = mt.resultType + + val paramAdaptationNeeded = + erasedSamParamTypes.lazyZip(overriddenParamTypes).exists((samType, overriddenType) => + !sameClass(samType, overriddenType) && (!autoAdaptedParam(samType) + // LambdaMetaFactory cannot auto-adapt between Object and Array types + || overriddenType.isInstanceOf[JavaArrayType])) + + val resultAdaptationNeeded = + !sameClass(erasedSamResultType, overriddenResultType) && !autoAdaptedResult(erasedSamResultType) + + !(paramAdaptationNeeded || resultAdaptationNeeded) + case _ => true + } + case _ => false + end samDoesNotNeedExpansion end TypeErasure import TypeErasure.* From eb46ceaca00ed929793da5201eec4eb1807a86dc Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Thu, 4 Dec 2025 22:17:54 +0100 Subject: [PATCH 8/8] Add new tests with fixed Unit return type --- tests/run/i24573.check | 6 ++++++ tests/run/i24573.scala | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/run/i24573.check b/tests/run/i24573.check index 65cf09ede65d..ae6671e527f9 100644 --- a/tests/run/i24573.check +++ b/tests/run/i24573.check @@ -35,3 +35,9 @@ 62 63 64 +71 +72 +75 +76 +81 +82 diff --git a/tests/run/i24573.scala b/tests/run/i24573.scala index a2f1e7525e15..d58ace91c98b 100644 --- a/tests/run/i24573.scala +++ b/tests/run/i24573.scala @@ -58,6 +58,17 @@ trait SFIS extends F1[Int, String]: trait SFIU extends F1[Int, Unit]: def apply(t: Int): Unit +trait F1U[-T]: + def apply(t: T): Unit + +trait SF2T[-T] extends F1U[T]: + def apply(t: T): Unit + +trait SF2I extends F1U[Int]: + def apply(t: Int): Unit + +trait SF2S extends F1U[String]: + def apply(t: String): Unit object Test: def main(args: Array[String]): Unit = @@ -149,3 +160,19 @@ object Test: val sfIU: SFIU = (x: Int) => println(x) // expanded sfIU(64) + val f2ITU: F1U[Int] = (x: Int) => println(x) // closure + f2ITU(71) + val f2STU: F1U[String] = (x: String) => println(x) // closure + f2STU("72") + + val sf2IT: SF2T[Int] = (x: Int) => println(x) // closure + sf2IT(75) + val sf2ST: SF2T[String] = (x: String) => println(x) // closure + sf2ST("76") + + val sf2I: SF2I = (x: Int) => println(x) // expanded + sf2I(81) + val sf2S: SF2S = (x: String) => println(x) // closure + sf2S("82") + +end Test