Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/config/JavaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
// Check if the SAM can be implemented via LambdaMetaFactory
TypeErasure.samNotNeededExpansion(cls)

/** We could get away with excluding BoxedBooleanClass for the
* purpose of equality testing since it need not compare equal
Expand Down
99 changes: 97 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -586,7 +586,102 @@ object TypeErasure {
defn.FunctionType(n = info.nonErasedParamCount)
}
erasure(functionType(applyInfo))
}

/** Check if LambdaMetaFactory can handle signature adaptation between two method types.
*
* LMF has limitations on what type adaptations it can perform automatically.
* This method checks whether manual bridging is needed for params and/or result.
*
* The adaptation rules are:
* - For parameters: primitives and value classes cannot be auto-adapted by LMF
* because the Scala spec requires null to be "unboxed" to the default value,
* but LMF throws `NullPointerException` instead.
* - For results: value classes and Unit cannot be auto-adapted by LMF.
* Non-Unit primitives can be auto-adapted since LMF only needs to box (not unbox).
* - LMF cannot auto-adapt between Object and Array types.
*
* @param implParamTypes Parameter types of the implementation method
* @param implResultType Result type of the implementation method
* @param samParamTypes Parameter types of the SAM method
* @param samResultType Result type of the SAM method
*
* @return (paramNeeded, resultNeeded) indicating what needs bridging
*/
def additionalAdaptationNeeded(
implParamTypes: List[Type],
implResultType: Type,
samParamTypes: List[Type],
samResultType: Type
)(using Context): (paramNeeded: Boolean, resultNeeded: Boolean) =
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(tp: Type) =
!tp.isErasedValueType && !(tp.classSymbol eq defn.UnitClass)

val paramAdaptationNeeded =
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
// LambdaMetaFactory cannot auto-adapt between Object and Array types
|| samType.isInstanceOf[JavaArrayType]))

val resultAdaptationNeeded =
!sameClass(implResultType, samResultType) && !autoAdaptedResult(implResultType)

(paramAdaptationNeeded, resultAdaptationNeeded)
end additionalAdaptationNeeded

/** 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
* to be qualifies as a valid functional interface on JVM.
* This method returns true if all overridden methods have compatible erased signatures
* that LMF can auto-adapt (or don't need adaptation).
*
* When this returns true, the SAM class does not need to be expanded.
*
* @param cls The SAM class to check
* @return true if LMF can handle the required adaptation
*/
def samNotNeededExpansion(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

samMethSym.allOverriddenSymbols.forall { overridden =>
val erasedOverriddenInfo = transformInfo(overridden, overridden.info)
erasedOverriddenInfo match
case mt: MethodType =>
val (paramNeeded, resultNeeded) =
additionalAdaptationNeeded(erasedSamParamTypes, erasedSamResultType, mt.paramInfos, mt.resultType)
!(paramNeeded || resultNeeded)
case _ => true
}
case _ => false
end samNotNeededExpansion
end TypeErasure

import TypeErasure.*

Expand Down
38 changes: 3 additions & 35 deletions compiler/src/dotty/tools/dotc/transform/Erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -453,41 +453,9 @@ object Erasure {
val samParamTypes = sam.paramInfos
val samResultType = sam.resultType

/** 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. See `lambda-null.scala`
* for test cases.
*
* @see [LambdaMetaFactory](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/LambdaMetafactory.html)
*/
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.isErasedValueType && !implReturnsUnit

def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol

val paramAdaptationNeeded =
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
// LambdaMetaFactory cannot auto-adapt between Object and Array types
|| samType.isInstanceOf[JavaArrayType]))
val resultAdaptationNeeded =
!sameClass(implResultType, samResultType) && !autoAdaptedResult
// Check if bridging is needed using the common function from TypeErasure
val (paramAdaptationNeeded, resultAdaptationNeeded) =
additionalAdaptationNeeded(implParamTypes, implResultType, samParamTypes, samResultType)

if paramAdaptationNeeded || resultAdaptationNeeded then
// Instead of instantiating `scala.FunctionN`, see if we can instantiate
Expand Down
43 changes: 43 additions & 0 deletions tests/run/i24573.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
1
2
3
11
12
13
14
15
16
17
18
19
20
21
22
23
24
31
32
33
34
41
42
43
44
45
46
51
52
53
55
56
57
61
62
63
64
71
72
75
76
81
82
178 changes: 178 additions & 0 deletions tests/run/i24573.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
trait ConTU[-T] extends (T => Unit):
def apply(t: T): Unit

trait ConTI[-T] extends (T => Int):
def apply(t: T): Int

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 SFTU[-T] extends F1[T, Unit]:
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 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 =
val fIU: (Int => Unit) = (x: Int) => println(x) // closure by JFunction1
fIU(1)

val fIS: (Int => String) = (x: Int) => x.toString // closure
println(fIS(2))

val fUI: (() => Int) = () => 3 // closure
println(fUI())

val conITU: ConTU[Int] = (x: Int) => println(x) // expanded
conITU(11)
val conITI: ConTI[Int] = (x: Int) => x // closure
println(conITI(12))
val conITS: ConTS[Int] = (x: Int) => x.toString // closure
println(conITS(13))
val conSTS: ConTS[String] = (x: String) => x // closure
println(conSTS("14"))

val conIRS: ConIR[String] = (x: Int) => x.toString // expanded
println(conIRS(15))
val conIRI: ConIR[Int] = (x: Int) => x // expanded
println(conIRI(16))
val conIRU: ConIR[Unit] = (x: Int) => println(x) // expanded
conIRU(17)

val conSRI: ConSR[Int] = (x: String) => x.toInt // closure
println(conSRI("18"))
val conURI: ConUR[Int] = () => 19 // closure
println(conURI())
val conURU: ConUR[Unit] = () => println("20") // closure
conURU()

val conII: ConII = (x: Int) => x // expanded
println(conII(21))
val conSI: ConSI = (x: String) => x.toInt // closure
println(conSI("22"))
val conIS: ConIS = (x: Int) => x.toString // expanded
println(conIS(23))
val conUU: ConUU = () => println("24") // expanded
conUU()

val ffIU: F1[Int, Unit] = (x: Int) => println(x) // closure
ffIU(31)
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) // expanded
sfITU(41)
val sfSTU: SFTU[String] = (x: String) => println(x) // expanded
sfSTU("42")

val sfITI: SFTI[Int] = (x: Int) => x // closure
println(sfITI(43))
val sfSTI: SFTI[String] = (x: String) => x.toInt // closure
println(sfSTI("44"))

val sfITS: SFTS[Int] = (x: Int) => x.toString // closure
println(sfITS(45))
val sfSTS: SFTS[String] = (x: String) => x // closure
println(sfSTS("46"))

val sfIRI: SFIR[Int] = (x: Int) => x // expanded
println(sfIRI(51))
val sfIRS: SFIR[String] = (x: Int) => x.toString // expanded
println(sfIRS(52))
val sfIRU: SFIR[Unit] = (x: Int) => println(x) // expanded
sfIRU(53)

val sfSRI: SFSR[Int] = (x: String) => x.toInt // closure
println(sfSRI("55"))
val sfSRS: SFSR[String] = (x: String) => x // closure
println(sfSRS("56"))
val sfSRU: SFSR[Unit] = (x: String) => println(x) // closure
sfSRU("57")

val sfII: SFII = (x: Int) => x // expanded
println(sfII(61))
val sfSI: SFSI = (x: String) => x.toInt // closure
println(sfSI("62"))
val sfIS: SFIS = (x: Int) => x.toString // expanded
println(sfIS(63))
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