Skip to content

Commit

Permalink
Merge pull request scala#11846 from dotty-staging/univ-array-erasure
Browse files Browse the repository at this point in the history
Array erasure: Better Java and Scala 2 compat
  • Loading branch information
odersky authored Mar 22, 2021
2 parents f2e0a4c + 91d174f commit 4035f51
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 35 deletions.
87 changes: 57 additions & 30 deletions compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -269,42 +269,70 @@ object TypeErasure {
}
}

/** Underlying type that does not contain aliases or abstract types
* at top-level, treating opaque aliases as transparent.
/** Is `Array[tp]` a generic Array that needs to be erased to `Object`?
* This is true if among the subtypes of `Array[tp]` there is either:
* - both a reference array type and a primitive array type
* (e.g. `Array[_ <: Int | String]`, `Array[_ <: Any]`)
* - or two different primitive array types (e.g. `Array[_ <: Int | Double]`)
* In both cases the erased lub of those array types on the JVM is `Object`.
*
* In addition, if `isScala2` is true, we mimic the Scala 2 erasure rules and
* also return true for element types upper-bounded by a non-reference type
* such as in `Array[_ <: Int]` or `Array[_ <: UniversalTrait]`.
*/
def classify(tp: Type)(using Context): Type =
if (tp.typeSymbol.isClass) tp
else tp match {
case tp: TypeProxy => classify(tp.translucentSuperType)
case tp: AndOrType => tp.derivedAndOrType(classify(tp.tp1), classify(tp.tp2))
case _ => tp
}
def isGenericArrayElement(tp: Type, isScala2: Boolean)(using Context): Boolean = {
/** A symbol that represents the sort of JVM array that values of type `t` can be stored in:
* - If we can always store such values in a reference array, return Object
* - If we can always store them in a specific primitive array, return the
* corresponding primitive class
* - Otherwise, return `NoSymbol`.
*/
def arrayUpperBound(t: Type): Symbol = t.dealias match
case t: TypeRef if t.symbol.isClass =>
val sym = t.symbol
// Only a few classes have both primitives and references as subclasses.
if (sym eq defn.AnyClass) || (sym eq defn.AnyValClass) || (sym eq defn.MatchableClass) || (sym eq defn.SingletonClass)
|| isScala2 && !(t.derivesFrom(defn.ObjectClass) || t.isNullType) then
NoSymbol
// We only need to check for primitives because derived value classes in arrays are always boxed.
else if sym.isPrimitiveValueClass then
sym
else
defn.ObjectClass
case tp: TypeProxy =>
arrayUpperBound(tp.translucentSuperType)
case tp: AndOrType =>
val repr1 = arrayUpperBound(tp.tp1)
val repr2 = arrayUpperBound(tp.tp2)
if repr1 eq repr2 then
repr1
else if tp.isAnd then
repr1.orElse(repr2)
else
NoSymbol
case _ =>
NoSymbol

/** Is `tp` an abstract type or polymorphic type parameter that has `Any`, `AnyVal`, `Null`,
* or a universal trait as upper bound and that is not Java defined? Arrays of such types are
* erased to `Object` instead of `Object[]`.
*/
def isUnboundedGeneric(tp: Type)(using Context): Boolean = {
def isBoundedType(t: Type): Boolean = t match {
case t: OrType => isBoundedType(t.tp1) && isBoundedType(t.tp2)
case _ => t.derivesFrom(defn.ObjectClass) || t.isNullType
}
/** Can one of the JVM Array type store all possible values of type `t`? */
def fitsInJVMArray(t: Type): Boolean = arrayUpperBound(t).exists

tp.dealias match {
case tp: TypeRef if !tp.symbol.isOpaqueAlias =>
!tp.symbol.isClass &&
!isBoundedType(classify(tp)) &&
!tp.symbol.is(JavaDefined)
!tp.symbol.is(JavaDefined) && // In Java code, Array[T] can never erase to Object
!fitsInJVMArray(tp)
case tp: TypeParamRef =>
!isBoundedType(classify(tp))
case tp: TypeAlias => isUnboundedGeneric(tp.alias)
!fitsInJVMArray(tp)
case tp: TypeAlias =>
isGenericArrayElement(tp.alias, isScala2)
case tp: TypeBounds =>
val upper = classify(tp.hi)
!isBoundedType(upper) &&
!upper.isPrimitiveValueType
case tp: TypeProxy => isUnboundedGeneric(tp.translucentSuperType)
case tp: AndType => isUnboundedGeneric(tp.tp1) && isUnboundedGeneric(tp.tp2)
case tp: OrType => isUnboundedGeneric(tp.tp1) || isUnboundedGeneric(tp.tp2)
!fitsInJVMArray(tp.hi)
case tp: TypeProxy =>
isGenericArrayElement(tp.translucentSuperType, isScala2)
case tp: AndType =>
isGenericArrayElement(tp.tp1, isScala2) && isGenericArrayElement(tp.tp2, isScala2)
case tp: OrType =>
isGenericArrayElement(tp.tp1, isScala2) || isGenericArrayElement(tp.tp2, isScala2)
case _ => false
}
}
Expand Down Expand Up @@ -642,8 +670,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst

private def eraseArray(tp: Type)(using Context) = {
val defn.ArrayOf(elemtp) = tp
if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType)
else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType
if (isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2)) defn.ObjectType
else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK)(elemtp))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import core.Flags._
import core.Names.{DerivedName, Name, SimpleName, TypeName}
import core.Symbols._
import core.TypeApplications.TypeParamInfo
import core.TypeErasure.{erasedGlb, erasure, isUnboundedGeneric}
import core.TypeErasure.{erasedGlb, erasure, isGenericArrayElement}
import core.Types._
import core.classfile.ClassfileConstants
import ast.Trees._
Expand Down Expand Up @@ -246,7 +246,7 @@ object GenericSignatures {
typeParamSig(ref.paramName.lastPart)

case defn.ArrayOf(elemtp) =>
if (isUnboundedGeneric(elemtp))
if (isGenericArrayElement(elemtp, isScala2 = false))
jsig(defn.ObjectType)
else
builder.append(ClassfileConstants.ARRAY_TAG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ object TypeTestsCasts {
transformTypeTest(e, tp1, flagUnrelated)
.and(transformTypeTest(e, tp2, flagUnrelated))
}
case defn.MultiArrayOf(elem, ndims) if isUnboundedGeneric(elem) =>
case defn.MultiArrayOf(elem, ndims) if isGenericArrayElement(elem, isScala2 = false) =>
def isArrayTest(arg: Tree) =
ref(defn.runtimeMethodRef(nme.isArray)).appliedTo(arg, Literal(Constant(ndims)))
if (ndims == 1) isArrayTest(expr)
Expand Down
42 changes: 42 additions & 0 deletions sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ trait B
trait SubB extends B
trait C
trait Cov[+T]
trait Univ extends Any

class D

Expand Down Expand Up @@ -153,4 +154,45 @@ class Z {
def int_61(x: Any with Int): Unit = {}
def int_62(x: Int with AnyVal): Unit = {}
def int_63(x: AnyVal with Int): Unit = {}

def intARRAY_64(x: Array[Int with Singleton]): Unit = {}
def intARRAY_65(x: Array[_ <: Int]): Unit = {}
def intARRAY_66(x: Array[_ <: Int with Singleton]): Unit = {}
def intARRAY_67(x: Array[_ <: Singleton with Int]): Unit = {}
def intARRAY_68(x: Array[_ <: Int with Any]): Unit = {}
def intARRAY_69(x: Array[_ <: Any with Int]): Unit = {}
def intARRAY_70(x: Array[_ <: Int with AnyVal]): Unit = {}
def intARRAY_71(x: Array[_ <: AnyVal with Int]): Unit = {}
def intARRAY_71a(x: Array[_ <: Int | Int]): Unit = {}
def intARRAY_71b(x: Array[_ <: 1 | 2]): Unit = {}

def stringARRAY_72(x: Array[String with Singleton]): Unit = {}
def stringARRAY_73(x: Array[_ <: String]): Unit = {}
def stringARRAY_74(x: Array[_ <: String with Singleton]): Unit = {}
def stringARRAY_75(x: Array[_ <: Singleton with String]): Unit = {}
def stringARRAY_76(x: Array[_ <: String with Any]): Unit = {}
def stringARRAY_77(x: Array[_ <: Any with String]): Unit = {}
def stringARRAY_78(x: Array[_ <: String with AnyRef]): Unit = {}
def stringARRAY_79(x: Array[_ <: AnyRef with String]): Unit = {}
def stringARRAY_79a(x: Array[_ <: String | String]): Unit = {}
def stringARRAY_79b(x: Array[_ <: "a" | "b"]): Unit = {}

def object_80(x: Array[_ <: Singleton]): Unit = {}
def object_81(x: Array[_ <: AnyVal]): Unit = {}
def objectARRAY_82(x: Array[_ <: AnyRef]): Unit = {}
def object_83(x: Array[_ <: Any]): Unit = {}
def object_83a(x: Array[_ <: Matchable]): Unit = {}
def object_83b(x: Array[_ <: Int | Double]): Unit = {}
def object_83c(x: Array[_ <: String | Int]): Unit = {}
def object_83d(x: Array[_ <: Int | Matchable]): Unit = {}
def object_83e(x: Array[_ <: AnyRef | AnyVal]): Unit = {}

def serializableARRAY_84(x: Array[_ <: Serializable]): Unit = {}
def univARRAY_85(x: Array[_ <: Univ]): Unit = {}
def aARRAY_86(x: Array[_ <: A]): Unit = {}
def aARRAY_87(x: Array[_ <: A with B]): Unit = {}

def objectARRAY_88(x: Array[Any]): Unit = {}
def objectARRAY_89(x: Array[AnyRef]): Unit = {}
def objectARRAY_90(x: Array[AnyVal]): Unit = {}
}
31 changes: 29 additions & 2 deletions sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,40 @@ object Main {
z.int_61(1)
z.int_62(1)
z.int_63(1)
z.intARRAY_64(dummy)
z.object_65(dummy)
z.object_66(dummy)
z.object_67(dummy)
z.object_68(dummy)
z.object_69(dummy)
z.object_70(dummy)
z.object_71(dummy)
z.stringARRAY_72(dummy)
z.stringARRAY_73(dummy)
z.stringARRAY_74(dummy)
z.stringARRAY_75(dummy)
z.stringARRAY_76(dummy)
z.stringARRAY_77(dummy)
z.stringARRAY_78(dummy)
z.stringARRAY_79(dummy)
z.object_80(dummy)
z.object_81(dummy)
z.objectARRAY_82(dummy)
z.object_83(dummy)
z.object_84(dummy)
z.object_85(dummy)
z.aARRAY_86(dummy)
z.aARRAY_87(dummy)
z.objectARRAY_88(dummy)
z.objectARRAY_89(dummy)
z.objectARRAY_90(dummy)

val methods = classOf[scala2Lib.Z].getDeclaredMethods.toList ++ classOf[dottyApp.Z].getDeclaredMethods.toList
methods.foreach { m =>
m.getName match {
case s"${prefix}_${suffix}" =>
val paramClass = m.getParameterTypes()(0).getSimpleName
assert(prefix == paramClass.toLowerCase, s"Method `$m` erased to `$paramClass` which does not match its prefix `$prefix`")
val paramClass = m.getParameterTypes()(0).getSimpleName.toLowerCase.replaceAll("""\[\]""", "ARRAY")
assert(prefix == paramClass, s"Method `$m` erased to `$paramClass` which does not match its prefix `$prefix`")
case _ =>
}
}
Expand Down
33 changes: 33 additions & 0 deletions sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ trait B
trait SubB extends B
trait C
trait Cov[+T]
trait Univ extends Any

class D

Expand Down Expand Up @@ -153,4 +154,36 @@ class Z {
def int_61(x: Any with Int): Unit = {}
def int_62(x: Int with AnyVal): Unit = {}
def int_63(x: AnyVal with Int): Unit = {}

def intARRAY_64(x: Array[Int with Singleton]): Unit = {}
def object_65(x: Array[_ <: Int]): Unit = {}
def object_66(x: Array[_ <: Int with Singleton]): Unit = {}
def object_67(x: Array[_ <: Singleton with Int]): Unit = {}
def object_68(x: Array[_ <: Int with Any]): Unit = {}
def object_69(x: Array[_ <: Any with Int]): Unit = {}
def object_70(x: Array[_ <: Int with AnyVal]): Unit = {}
def object_71(x: Array[_ <: AnyVal with Int]): Unit = {}

def stringARRAY_72(x: Array[String with Singleton]): Unit = {}
def stringARRAY_73(x: Array[_ <: String]): Unit = {}
def stringARRAY_74(x: Array[_ <: String with Singleton]): Unit = {}
def stringARRAY_75(x: Array[_ <: Singleton with String]): Unit = {}
def stringARRAY_76(x: Array[_ <: String with Any]): Unit = {}
def stringARRAY_77(x: Array[_ <: Any with String]): Unit = {}
def stringARRAY_78(x: Array[_ <: String with AnyRef]): Unit = {}
def stringARRAY_79(x: Array[_ <: AnyRef with String]): Unit = {}

def object_80(x: Array[_ <: Singleton]): Unit = {}
def object_81(x: Array[_ <: AnyVal]): Unit = {}
def objectARRAY_82(x: Array[_ <: AnyRef]): Unit = {}
def object_83(x: Array[_ <: Any]): Unit = {}

def object_84(x: Array[_ <: Serializable]): Unit = {}
def object_85(x: Array[_ <: Univ]): Unit = {}
def aARRAY_86(x: Array[_ <: A]): Unit = {}
def aARRAY_87(x: Array[_ <: A with B]): Unit = {}

def objectARRAY_88(x: Array[Any]): Unit = {}
def objectARRAY_89(x: Array[AnyRef]): Unit = {}
def objectARRAY_90(x: Array[AnyVal]): Unit = {}
}
24 changes: 24 additions & 0 deletions tests/run/array-erasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,35 @@ object Test {
}
}

def arr3[T <: Int](x: Array[T]) = {
x(0) == 2
x.sameElements(x)
}

def arr4[T <: Int | Double](x: Array[T]) = {
x(0) == 2
x.sameElements(x)
}

def arr5[T <: Int | String](x: Array[T]) = {
x(0) == 2
x.sameElements(x)
}

def arr6[T <: Matchable](x: Array[T]) = {
x(0) == 2
x.sameElements(x)
}

def main(args: Array[String]): Unit = {
val x: Array[Int] = Array(0)

arr0(x)
arr1(x)
arr2(x)
arr3(x)
arr4(x)
arr5(x)
arr6(x)
}
}
4 changes: 4 additions & 0 deletions tests/run/arrays-from-java/A_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class A {
def foo1[T <: Serializable](x: Array[T]): Unit = {}
def foo2[T <: Object & Serializable](x: Array[T]): Unit = {}
}
21 changes: 21 additions & 0 deletions tests/run/arrays-from-java/C_2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import java.io.Serializable;

class B extends A {
@Override
public <T extends Serializable> void foo1(T[] x) {}
@Override
public <T extends Object & Serializable> void foo2(T[] x) {}
}

public class C_2 {
public static void test() {
A a = new A();
B b = new B();
String[] arr = { "" };
a.foo1(arr);
a.foo2(arr);

b.foo1(arr);
b.foo2(arr);
}
}
4 changes: 4 additions & 0 deletions tests/run/arrays-from-java/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
object Test {
def main(args: Array[String]): Unit =
C_2.test()
}

0 comments on commit 4035f51

Please sign in to comment.