Skip to content

Fix erasure of Java intersection without a class #12603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2021
Merged
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
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeApplications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 3 additions & 5 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
19 changes: 6 additions & 13 deletions compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions tests/run/java-intersection.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
1
2
1
Sub1
2
Sub2
10 changes: 9 additions & 1 deletion tests/run/java-intersection/A_1.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import java.io.Serializable;

public class A_1 {
public <T extends Object & java.io.Serializable> void foo(T x) {}
public <T extends Object & Serializable> void foo(T x) {
System.out.println("1");
}

public <T extends Cloneable & Serializable> void foo(T x) {
System.out.println("2");
}
}
21 changes: 20 additions & 1 deletion tests/run/java-intersection/Test_2.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}