Skip to content

Commit 00f3089

Browse files
authored
Fix #24573: Add stricter checks for platform SAM compatibility (#24624)
This PR fixes issue #24573 by adding stricter checks for platform SAM compatibility. The regression issue starts from version 3.8.0 where `Function1` and related traits gained the `NoInits` flag (I guess due to the way we read from jvm bytecode or from tasty?). This changes how the compiler determines whether a SAM type qualifies as a platform SAM. When a trait is classified as a Java platform SAM, the compiler uses `invokedynamic` with `LambdaMetaFactory` (LMF) to create the lambda at runtime, rather than expanding it into an anonymous class at compile time. This is more efficient, but LMF has limitations on what type adaptations it can perform automatically. It turns out the logic regarding SAM traits extending other traits has problems from the beginning, and even in Scala 2. The code from the issue works before 3.8 is totally by accident (because we always expand the SAM trait extending `FunctionN`). We can reproduce the runtime exception using a custom function class (in Scala 2, and Scala 3.0~8): ```scala trait F1[-T, +R]: def apply(t: T): R trait SF[-T] extends F1[T, Unit]: def apply(t: T): Unit @main def Test = val sf: SF[String] = i => println(i) sf("") // exception here // Caused by: java.lang.invoke.LambdaConversionException: //Type mismatch for lambda expected return: void is not convertible to class java.lang.Object ``` From the view of Scala, `SF` is a valid SAM, because it has one abstract function `def apply(t: T): Unit`. However, after erasure, the `apply` of `F1` becomes `Object apply(Object)`, and the apply of `SF` becomes `void apply(Object)`, which are two different abstract functions; hence, `SF` is not qualifies as a function interface on JVM. This is not only for `Unit` type, any primitive number type can cause the similar problem. Therefore, we have to expand the rhs of `val sf: SF[String] = i => println(i)` into an anonymous class and implement all the bridge functions. This PR strengthen the SAM check on Java platform by adding a function called `samNotNeededExpansion` to check if the SAM method's erased signature is compatible with all overridden methods. It mirrors the logic in `Erasure.Boxing.adaptClosure` to determine what LMF can and cannot auto-adapt.
2 parents 14909dd + b1130ab commit 00f3089

File tree

5 files changed

+339
-38
lines changed

5 files changed

+339
-38
lines changed

compiler/src/dotty/tools/dotc/config/JavaPlatform.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ class JavaPlatform extends Platform {
5050
cls.superClass == defn.ObjectClass &&
5151
cls.directlyInheritedTraits.forall(_.is(NoInits)) &&
5252
!ExplicitOuter.needsOuterIfReferenced(cls) &&
53-
cls.typeRef.fields.isEmpty // Superaccessors already show up as abstract methods here, so no test necessary
53+
// Superaccessors already show up as abstract methods here, so no test necessary
54+
cls.typeRef.fields.isEmpty &&
55+
// Check if the SAM can be implemented via LambdaMetaFactory
56+
TypeErasure.samExpansionNotNeeded(cls)
5457

5558
/** We could get away with excluding BoxedBooleanClass for the
5659
* purpose of equality testing since it need not compare equal

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ end SourceLanguage
7474
* only for isInstanceOf, asInstanceOf: PolyType, TypeParamRef, TypeBounds
7575
*
7676
*/
77-
object TypeErasure {
77+
object TypeErasure:
7878

7979
private def erasureDependsOnArgs(sym: Symbol)(using Context) =
8080
sym == defn.ArrayClass || sym == defn.PairClass || sym.isDerivedValueClass
@@ -586,7 +586,102 @@ object TypeErasure {
586586
defn.FunctionType(n = info.nonErasedParamCount)
587587
}
588588
erasure(functionType(applyInfo))
589-
}
589+
590+
/** Check if LambdaMetaFactory can handle signature adaptation between two method types.
591+
*
592+
* LMF has limitations on what type adaptations it can perform automatically.
593+
* This method checks whether manual bridging is needed for params and/or result.
594+
*
595+
* The adaptation rules are:
596+
* - For parameters: primitives and value classes cannot be auto-adapted by LMF
597+
* because the Scala spec requires null to be "unboxed" to the default value,
598+
* but LMF throws `NullPointerException` instead.
599+
* - For results: value classes and Unit cannot be auto-adapted by LMF.
600+
* Non-Unit primitives can be auto-adapted since LMF only needs to box (not unbox).
601+
* - LMF cannot auto-adapt between Object and Array types.
602+
*
603+
* @param implParamTypes Parameter types of the implementation method
604+
* @param implResultType Result type of the implementation method
605+
* @param samParamTypes Parameter types of the SAM method
606+
* @param samResultType Result type of the SAM method
607+
*
608+
* @return (paramNeeded, resultNeeded) indicating what needs bridging
609+
*/
610+
def additionalAdaptationNeeded(
611+
implParamTypes: List[Type],
612+
implResultType: Type,
613+
samParamTypes: List[Type],
614+
samResultType: Type
615+
)(using Context): (paramNeeded: Boolean, resultNeeded: Boolean) =
616+
def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol
617+
618+
/** Can the implementation parameter type `tp` be auto-adapted to a different
619+
* parameter type in the SAM?
620+
*
621+
* For derived value classes, we always need to do the bridging manually.
622+
* For primitives, we cannot rely on auto-adaptation on the JVM because
623+
* the Scala spec requires null to be "unboxed" to the default value of
624+
* the value class, but the adaptation performed by LambdaMetaFactory
625+
* will throw a `NullPointerException` instead.
626+
*/
627+
def autoAdaptedParam(tp: Type) = !tp.isErasedValueType && !tp.isPrimitiveValueType
628+
629+
/** Can the implementation result type be auto-adapted to a different result
630+
* type in the SAM?
631+
*
632+
* For derived value classes, it's the same story as for parameters.
633+
* For non-Unit primitives, we can actually rely on the `LambdaMetaFactory`
634+
* adaptation, because it only needs to box, not unbox, so no special
635+
* handling of null is required.
636+
*/
637+
def autoAdaptedResult(tp: Type) =
638+
!tp.isErasedValueType && !(tp.classSymbol eq defn.UnitClass)
639+
640+
val paramAdaptationNeeded =
641+
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
642+
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
643+
// LambdaMetaFactory cannot auto-adapt between Object and Array types
644+
|| samType.isInstanceOf[JavaArrayType]))
645+
646+
val resultAdaptationNeeded =
647+
!sameClass(implResultType, samResultType) && !autoAdaptedResult(implResultType)
648+
649+
(paramAdaptationNeeded, resultAdaptationNeeded)
650+
end additionalAdaptationNeeded
651+
652+
/** Check if LambdaMetaFactory can handle the SAM method's required signature adaptation.
653+
*
654+
* When a SAM method overrides other methods, the erased signatures must be compatible
655+
* to be qualifies as a valid functional interface on JVM.
656+
* This method returns true if all overridden methods have compatible erased signatures
657+
* that LMF can auto-adapt (or don't need adaptation).
658+
*
659+
* When this returns true, the SAM class does not need to be expanded.
660+
*
661+
* @param cls The SAM class to check
662+
* @return true if LMF can handle the required adaptation
663+
*/
664+
def samExpansionNotNeeded(cls: ClassSymbol)(using Context): Boolean = cls.typeRef.possibleSamMethods match
665+
case Seq(samMeth) =>
666+
val samMethSym = samMeth.symbol
667+
val erasedSamInfo = transformInfo(samMethSym, samMeth.info)
668+
669+
val (erasedSamParamTypes, erasedSamResultType) = erasedSamInfo match
670+
case mt: MethodType => (mt.paramInfos, mt.resultType)
671+
case _ => return false
672+
673+
samMethSym.allOverriddenSymbols.forall { overridden =>
674+
val erasedOverriddenInfo = transformInfo(overridden, overridden.info)
675+
erasedOverriddenInfo match
676+
case mt: MethodType =>
677+
val (paramNeeded, resultNeeded) =
678+
additionalAdaptationNeeded(erasedSamParamTypes, erasedSamResultType, mt.paramInfos, mt.resultType)
679+
!(paramNeeded || resultNeeded)
680+
case _ => true
681+
}
682+
case _ => false
683+
end samExpansionNotNeeded
684+
end TypeErasure
590685

591686
import TypeErasure.*
592687

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -453,41 +453,9 @@ object Erasure {
453453
val samParamTypes = sam.paramInfos
454454
val samResultType = sam.resultType
455455

456-
/** Can the implementation parameter type `tp` be auto-adapted to a different
457-
* parameter type in the SAM?
458-
*
459-
* For derived value classes, we always need to do the bridging manually.
460-
* For primitives, we cannot rely on auto-adaptation on the JVM because
461-
* the Scala spec requires null to be "unboxed" to the default value of
462-
* the value class, but the adaptation performed by LambdaMetaFactory
463-
* will throw a `NullPointerException` instead. See `lambda-null.scala`
464-
* for test cases.
465-
*
466-
* @see [LambdaMetaFactory](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/LambdaMetafactory.html)
467-
*/
468-
def autoAdaptedParam(tp: Type) =
469-
!tp.isErasedValueType && !tp.isPrimitiveValueType
470-
471-
/** Can the implementation result type be auto-adapted to a different result
472-
* type in the SAM?
473-
*
474-
* For derived value classes, it's the same story as for parameters.
475-
* For non-Unit primitives, we can actually rely on the `LambdaMetaFactory`
476-
* adaptation, because it only needs to box, not unbox, so no special
477-
* handling of null is required.
478-
*/
479-
def autoAdaptedResult =
480-
!implResultType.isErasedValueType && !implReturnsUnit
481-
482-
def sameClass(tp1: Type, tp2: Type) = tp1.classSymbol == tp2.classSymbol
483-
484-
val paramAdaptationNeeded =
485-
implParamTypes.lazyZip(samParamTypes).exists((implType, samType) =>
486-
!sameClass(implType, samType) && (!autoAdaptedParam(implType)
487-
// LambdaMetaFactory cannot auto-adapt between Object and Array types
488-
|| samType.isInstanceOf[JavaArrayType]))
489-
val resultAdaptationNeeded =
490-
!sameClass(implResultType, samResultType) && !autoAdaptedResult
456+
// Check if bridging is needed using the common function from TypeErasure
457+
val (paramAdaptationNeeded, resultAdaptationNeeded) =
458+
additionalAdaptationNeeded(implParamTypes, implResultType, samParamTypes, samResultType)
491459

492460
if paramAdaptationNeeded || resultAdaptationNeeded then
493461
// Instead of instantiating `scala.FunctionN`, see if we can instantiate

tests/run/i24573.check

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
1
2+
2
3+
3
4+
11
5+
12
6+
13
7+
14
8+
15
9+
16
10+
17
11+
18
12+
19
13+
20
14+
21
15+
22
16+
23
17+
24
18+
25
19+
31
20+
32
21+
33
22+
34
23+
41
24+
42
25+
43
26+
44
27+
45
28+
46
29+
51
30+
52
31+
53
32+
55
33+
56
34+
57
35+
61
36+
62
37+
63
38+
64
39+
65
40+
71
41+
72
42+
75
43+
76
44+
81
45+
82

0 commit comments

Comments
 (0)