From 8a96cde231419a329d0070142febf642796cf86e Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 25 May 2021 19:44:14 +0200 Subject: [PATCH] Fix erasure of Java intersection without a class When I implemented our erasure algorithm in https://github.com/lampepfl/dotty/pull/11808, I misread https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.4 and incorrectly thought that a Java intersection had to contain at least one class, and since we always erase a Scala 3 intersection containing a class to that class, I thought we could use the Scala 3 intersection algorithm to erase Java intersections. But my assumption was incorrect: a Java intersection can in fact contain only interfaces, so we need to special-case them in erasure like before. Fixes #12586. --- .../tools/dotc/core/TypeApplications.scala | 3 ++- .../dotty/tools/dotc/core/TypeErasure.scala | 8 +++---- .../dotc/transform/GenericSignatures.scala | 19 ++++++----------- tests/run/java-intersection.check | 6 ++++++ tests/run/java-intersection/A_1.java | 10 ++++++++- tests/run/java-intersection/Test_2.scala | 21 ++++++++++++++++++- 6 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 tests/run/java-intersection.check diff --git a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala index 4e360224a18f..aa380b574b98 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeApplications.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeApplications.scala @@ -466,7 +466,8 @@ class TypeApplications(val self: Type) extends AnyVal { def translateJavaArrayElementType(using Context): Type = // A type parameter upper-bounded solely by `FromJavaObject` has `ObjectClass` as its classSymbol if self.typeSymbol.isAbstractOrParamType && (self.classSymbol eq defn.ObjectClass) then - AndType(self, defn.ObjectType) + // The order is important here since Java intersections erase to their first operand + AndType(defn.ObjectType, self) else self diff --git a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala index ab5c4fa5bf07..8f0a8c9df985 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeErasure.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeErasure.scala @@ -427,10 +427,6 @@ object TypeErasure { * This operation has the following the properties: * - Associativity and commutativity, because this method acts as the minimum * of the total order induced by `compareErasedGlb`. - * - Java compatibility: intersections that would be valid in Java code are - * erased like javac would erase them (a Java intersection is composed of - * exactly one class and one or more interfaces and always erases to the - * class). */ def erasedGlb(tp1: Type, tp2: Type)(using Context): Type = if compareErasedGlb(tp1, tp2) <= 0 then tp1 else tp2 @@ -609,7 +605,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst case tp: TypeProxy => this(tp.underlying) case tp @ AndType(tp1, tp2) => - if sourceLanguage.isScala2 then + if sourceLanguage.isJava then + this(tp1) + else if sourceLanguage.isScala2 then this(Scala2Erasure.intersectionDominator(Scala2Erasure.flattenedParents(tp))) else erasedGlb(this(tp1), this(tp2)) diff --git a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala index 4348bee27ff5..9024296eae89 100644 --- a/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala +++ b/compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala @@ -81,21 +81,14 @@ object GenericSignatures { val (repr :: _, others) = splitIntersection(bounds) builder.append(':') - // According to the Java spec - // (https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.4), - // intersections erase to their first member and must start with a class. - // So, if our intersection erases to a trait, in theory we should emit - // just that trait in the generic signature even if the intersection type - // is composed of multiple traits. But in practice Scala 2 has always - // ignored this restriction as intersections of traits seem to be handled - // correctly by javac, we do the same here since type soundness seems - // more important than adhering to the spec. + // In Java, intersections always erase to their first member, so put + // whatever parent erases to the Scala intersection erasure first in the + // signature. if repr.classSymbol.is(Trait) then + // An initial ':' is needed if the intersection starts with an interface + // (cf https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-TypeParameter) builder.append(':') - boxedSig(repr) - // If we wanted to be compliant with the spec, we would `return` here. - else - boxedSig(repr) + boxedSig(repr) others.filter(_.classSymbol.is(Trait)).foreach { tp => builder.append(':') boxedSig(tp) diff --git a/tests/run/java-intersection.check b/tests/run/java-intersection.check new file mode 100644 index 000000000000..51c75c6b280b --- /dev/null +++ b/tests/run/java-intersection.check @@ -0,0 +1,6 @@ +1 +2 +1 +Sub1 +2 +Sub2 diff --git a/tests/run/java-intersection/A_1.java b/tests/run/java-intersection/A_1.java index ebe834a74cce..f54862e8adc2 100644 --- a/tests/run/java-intersection/A_1.java +++ b/tests/run/java-intersection/A_1.java @@ -1,3 +1,11 @@ +import java.io.Serializable; + public class A_1 { - public void foo(T x) {} + public void foo(T x) { + System.out.println("1"); + } + + public void foo(T x) { + System.out.println("2"); + } } diff --git a/tests/run/java-intersection/Test_2.scala b/tests/run/java-intersection/Test_2.scala index cbc39988340a..a3d7b11fce12 100644 --- a/tests/run/java-intersection/Test_2.scala +++ b/tests/run/java-intersection/Test_2.scala @@ -1,7 +1,26 @@ +import java.io.Serializable + +class Sub extends A_1 { + override def foo[T <: Object & Serializable](x: T) = + super.foo(x) + println("Sub1") + + override def foo[T <: Cloneable & Serializable](x: T) = + super.foo(x) + println("Sub2") +} + object Test { def main(args: Array[String]): Unit = { + val x: Object & Serializable = new Serializable {} + val y: Cloneable & Serializable = new Cloneable with java.io.Serializable {} + val a = new A_1 - val x: java.io.Serializable = new java.io.Serializable {} a.foo(x) + a.foo(y) + + val s = new Sub + s.foo(x) + s.foo(y) } }