Skip to content

Commit

Permalink
Fix erasure of Java intersection without a class
Browse files Browse the repository at this point in the history
When I implemented our erasure algorithm in
scala#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 scala#12586.
  • Loading branch information
smarter committed May 25, 2021
1 parent 1f4d125 commit 8a96cde
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 21 deletions.
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)
}
}

0 comments on commit 8a96cde

Please sign in to comment.