Skip to content

Commit 4035f51

Browse files
authored
Merge pull request #11846 from dotty-staging/univ-array-erasure
Array erasure: Better Java and Scala 2 compat
2 parents f2e0a4c + 91d174f commit 4035f51

File tree

10 files changed

+217
-35
lines changed

10 files changed

+217
-35
lines changed

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

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -269,42 +269,70 @@ object TypeErasure {
269269
}
270270
}
271271

272-
/** Underlying type that does not contain aliases or abstract types
273-
* at top-level, treating opaque aliases as transparent.
272+
/** Is `Array[tp]` a generic Array that needs to be erased to `Object`?
273+
* This is true if among the subtypes of `Array[tp]` there is either:
274+
* - both a reference array type and a primitive array type
275+
* (e.g. `Array[_ <: Int | String]`, `Array[_ <: Any]`)
276+
* - or two different primitive array types (e.g. `Array[_ <: Int | Double]`)
277+
* In both cases the erased lub of those array types on the JVM is `Object`.
278+
*
279+
* In addition, if `isScala2` is true, we mimic the Scala 2 erasure rules and
280+
* also return true for element types upper-bounded by a non-reference type
281+
* such as in `Array[_ <: Int]` or `Array[_ <: UniversalTrait]`.
274282
*/
275-
def classify(tp: Type)(using Context): Type =
276-
if (tp.typeSymbol.isClass) tp
277-
else tp match {
278-
case tp: TypeProxy => classify(tp.translucentSuperType)
279-
case tp: AndOrType => tp.derivedAndOrType(classify(tp.tp1), classify(tp.tp2))
280-
case _ => tp
281-
}
283+
def isGenericArrayElement(tp: Type, isScala2: Boolean)(using Context): Boolean = {
284+
/** A symbol that represents the sort of JVM array that values of type `t` can be stored in:
285+
* - If we can always store such values in a reference array, return Object
286+
* - If we can always store them in a specific primitive array, return the
287+
* corresponding primitive class
288+
* - Otherwise, return `NoSymbol`.
289+
*/
290+
def arrayUpperBound(t: Type): Symbol = t.dealias match
291+
case t: TypeRef if t.symbol.isClass =>
292+
val sym = t.symbol
293+
// Only a few classes have both primitives and references as subclasses.
294+
if (sym eq defn.AnyClass) || (sym eq defn.AnyValClass) || (sym eq defn.MatchableClass) || (sym eq defn.SingletonClass)
295+
|| isScala2 && !(t.derivesFrom(defn.ObjectClass) || t.isNullType) then
296+
NoSymbol
297+
// We only need to check for primitives because derived value classes in arrays are always boxed.
298+
else if sym.isPrimitiveValueClass then
299+
sym
300+
else
301+
defn.ObjectClass
302+
case tp: TypeProxy =>
303+
arrayUpperBound(tp.translucentSuperType)
304+
case tp: AndOrType =>
305+
val repr1 = arrayUpperBound(tp.tp1)
306+
val repr2 = arrayUpperBound(tp.tp2)
307+
if repr1 eq repr2 then
308+
repr1
309+
else if tp.isAnd then
310+
repr1.orElse(repr2)
311+
else
312+
NoSymbol
313+
case _ =>
314+
NoSymbol
282315

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

293319
tp.dealias match {
294320
case tp: TypeRef if !tp.symbol.isOpaqueAlias =>
295321
!tp.symbol.isClass &&
296-
!isBoundedType(classify(tp)) &&
297-
!tp.symbol.is(JavaDefined)
322+
!tp.symbol.is(JavaDefined) && // In Java code, Array[T] can never erase to Object
323+
!fitsInJVMArray(tp)
298324
case tp: TypeParamRef =>
299-
!isBoundedType(classify(tp))
300-
case tp: TypeAlias => isUnboundedGeneric(tp.alias)
325+
!fitsInJVMArray(tp)
326+
case tp: TypeAlias =>
327+
isGenericArrayElement(tp.alias, isScala2)
301328
case tp: TypeBounds =>
302-
val upper = classify(tp.hi)
303-
!isBoundedType(upper) &&
304-
!upper.isPrimitiveValueType
305-
case tp: TypeProxy => isUnboundedGeneric(tp.translucentSuperType)
306-
case tp: AndType => isUnboundedGeneric(tp.tp1) && isUnboundedGeneric(tp.tp2)
307-
case tp: OrType => isUnboundedGeneric(tp.tp1) || isUnboundedGeneric(tp.tp2)
329+
!fitsInJVMArray(tp.hi)
330+
case tp: TypeProxy =>
331+
isGenericArrayElement(tp.translucentSuperType, isScala2)
332+
case tp: AndType =>
333+
isGenericArrayElement(tp.tp1, isScala2) && isGenericArrayElement(tp.tp2, isScala2)
334+
case tp: OrType =>
335+
isGenericArrayElement(tp.tp1, isScala2) || isGenericArrayElement(tp.tp2, isScala2)
308336
case _ => false
309337
}
310338
}
@@ -642,8 +670,7 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
642670

643671
private def eraseArray(tp: Type)(using Context) = {
644672
val defn.ArrayOf(elemtp) = tp
645-
if (classify(elemtp).derivesFrom(defn.NullClass)) JavaArrayType(defn.ObjectType)
646-
else if (isUnboundedGeneric(elemtp) && !sourceLanguage.isJava) defn.ObjectType
673+
if (isGenericArrayElement(elemtp, isScala2 = sourceLanguage.isScala2)) defn.ObjectType
647674
else JavaArrayType(erasureFn(sourceLanguage, semiEraseVCs = false, isConstructor, isSymbol, wildcardOK)(elemtp))
648675
}
649676

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import core.Flags._
1010
import core.Names.{DerivedName, Name, SimpleName, TypeName}
1111
import core.Symbols._
1212
import core.TypeApplications.TypeParamInfo
13-
import core.TypeErasure.{erasedGlb, erasure, isUnboundedGeneric}
13+
import core.TypeErasure.{erasedGlb, erasure, isGenericArrayElement}
1414
import core.Types._
1515
import core.classfile.ClassfileConstants
1616
import ast.Trees._
@@ -246,7 +246,7 @@ object GenericSignatures {
246246
typeParamSig(ref.paramName.lastPart)
247247

248248
case defn.ArrayOf(elemtp) =>
249-
if (isUnboundedGeneric(elemtp))
249+
if (isGenericArrayElement(elemtp, isScala2 = false))
250250
jsig(defn.ObjectType)
251251
else
252252
builder.append(ClassfileConstants.ARRAY_TAG)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ object TypeTestsCasts {
322322
transformTypeTest(e, tp1, flagUnrelated)
323323
.and(transformTypeTest(e, tp2, flagUnrelated))
324324
}
325-
case defn.MultiArrayOf(elem, ndims) if isUnboundedGeneric(elem) =>
325+
case defn.MultiArrayOf(elem, ndims) if isGenericArrayElement(elem, isScala2 = false) =>
326326
def isArrayTest(arg: Tree) =
327327
ref(defn.runtimeMethodRef(nme.isArray)).appliedTo(arg, Literal(Constant(ndims)))
328328
if (ndims == 1) isArrayTest(expr)

sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Api.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ trait B
88
trait SubB extends B
99
trait C
1010
trait Cov[+T]
11+
trait Univ extends Any
1112

1213
class D
1314

@@ -153,4 +154,45 @@ class Z {
153154
def int_61(x: Any with Int): Unit = {}
154155
def int_62(x: Int with AnyVal): Unit = {}
155156
def int_63(x: AnyVal with Int): Unit = {}
157+
158+
def intARRAY_64(x: Array[Int with Singleton]): Unit = {}
159+
def intARRAY_65(x: Array[_ <: Int]): Unit = {}
160+
def intARRAY_66(x: Array[_ <: Int with Singleton]): Unit = {}
161+
def intARRAY_67(x: Array[_ <: Singleton with Int]): Unit = {}
162+
def intARRAY_68(x: Array[_ <: Int with Any]): Unit = {}
163+
def intARRAY_69(x: Array[_ <: Any with Int]): Unit = {}
164+
def intARRAY_70(x: Array[_ <: Int with AnyVal]): Unit = {}
165+
def intARRAY_71(x: Array[_ <: AnyVal with Int]): Unit = {}
166+
def intARRAY_71a(x: Array[_ <: Int | Int]): Unit = {}
167+
def intARRAY_71b(x: Array[_ <: 1 | 2]): Unit = {}
168+
169+
def stringARRAY_72(x: Array[String with Singleton]): Unit = {}
170+
def stringARRAY_73(x: Array[_ <: String]): Unit = {}
171+
def stringARRAY_74(x: Array[_ <: String with Singleton]): Unit = {}
172+
def stringARRAY_75(x: Array[_ <: Singleton with String]): Unit = {}
173+
def stringARRAY_76(x: Array[_ <: String with Any]): Unit = {}
174+
def stringARRAY_77(x: Array[_ <: Any with String]): Unit = {}
175+
def stringARRAY_78(x: Array[_ <: String with AnyRef]): Unit = {}
176+
def stringARRAY_79(x: Array[_ <: AnyRef with String]): Unit = {}
177+
def stringARRAY_79a(x: Array[_ <: String | String]): Unit = {}
178+
def stringARRAY_79b(x: Array[_ <: "a" | "b"]): Unit = {}
179+
180+
def object_80(x: Array[_ <: Singleton]): Unit = {}
181+
def object_81(x: Array[_ <: AnyVal]): Unit = {}
182+
def objectARRAY_82(x: Array[_ <: AnyRef]): Unit = {}
183+
def object_83(x: Array[_ <: Any]): Unit = {}
184+
def object_83a(x: Array[_ <: Matchable]): Unit = {}
185+
def object_83b(x: Array[_ <: Int | Double]): Unit = {}
186+
def object_83c(x: Array[_ <: String | Int]): Unit = {}
187+
def object_83d(x: Array[_ <: Int | Matchable]): Unit = {}
188+
def object_83e(x: Array[_ <: AnyRef | AnyVal]): Unit = {}
189+
190+
def serializableARRAY_84(x: Array[_ <: Serializable]): Unit = {}
191+
def univARRAY_85(x: Array[_ <: Univ]): Unit = {}
192+
def aARRAY_86(x: Array[_ <: A]): Unit = {}
193+
def aARRAY_87(x: Array[_ <: A with B]): Unit = {}
194+
195+
def objectARRAY_88(x: Array[Any]): Unit = {}
196+
def objectARRAY_89(x: Array[AnyRef]): Unit = {}
197+
def objectARRAY_90(x: Array[AnyVal]): Unit = {}
156198
}

sbt-dotty/sbt-test/scala2-compat/erasure/dottyApp/Main.scala

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,40 @@ object Main {
7474
z.int_61(1)
7575
z.int_62(1)
7676
z.int_63(1)
77+
z.intARRAY_64(dummy)
78+
z.object_65(dummy)
79+
z.object_66(dummy)
80+
z.object_67(dummy)
81+
z.object_68(dummy)
82+
z.object_69(dummy)
83+
z.object_70(dummy)
84+
z.object_71(dummy)
85+
z.stringARRAY_72(dummy)
86+
z.stringARRAY_73(dummy)
87+
z.stringARRAY_74(dummy)
88+
z.stringARRAY_75(dummy)
89+
z.stringARRAY_76(dummy)
90+
z.stringARRAY_77(dummy)
91+
z.stringARRAY_78(dummy)
92+
z.stringARRAY_79(dummy)
93+
z.object_80(dummy)
94+
z.object_81(dummy)
95+
z.objectARRAY_82(dummy)
96+
z.object_83(dummy)
97+
z.object_84(dummy)
98+
z.object_85(dummy)
99+
z.aARRAY_86(dummy)
100+
z.aARRAY_87(dummy)
101+
z.objectARRAY_88(dummy)
102+
z.objectARRAY_89(dummy)
103+
z.objectARRAY_90(dummy)
77104

78105
val methods = classOf[scala2Lib.Z].getDeclaredMethods.toList ++ classOf[dottyApp.Z].getDeclaredMethods.toList
79106
methods.foreach { m =>
80107
m.getName match {
81108
case s"${prefix}_${suffix}" =>
82-
val paramClass = m.getParameterTypes()(0).getSimpleName
83-
assert(prefix == paramClass.toLowerCase, s"Method `$m` erased to `$paramClass` which does not match its prefix `$prefix`")
109+
val paramClass = m.getParameterTypes()(0).getSimpleName.toLowerCase.replaceAll("""\[\]""", "ARRAY")
110+
assert(prefix == paramClass, s"Method `$m` erased to `$paramClass` which does not match its prefix `$prefix`")
84111
case _ =>
85112
}
86113
}

sbt-dotty/sbt-test/scala2-compat/erasure/scala2Lib/Api.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ trait B
88
trait SubB extends B
99
trait C
1010
trait Cov[+T]
11+
trait Univ extends Any
1112

1213
class D
1314

@@ -153,4 +154,36 @@ class Z {
153154
def int_61(x: Any with Int): Unit = {}
154155
def int_62(x: Int with AnyVal): Unit = {}
155156
def int_63(x: AnyVal with Int): Unit = {}
157+
158+
def intARRAY_64(x: Array[Int with Singleton]): Unit = {}
159+
def object_65(x: Array[_ <: Int]): Unit = {}
160+
def object_66(x: Array[_ <: Int with Singleton]): Unit = {}
161+
def object_67(x: Array[_ <: Singleton with Int]): Unit = {}
162+
def object_68(x: Array[_ <: Int with Any]): Unit = {}
163+
def object_69(x: Array[_ <: Any with Int]): Unit = {}
164+
def object_70(x: Array[_ <: Int with AnyVal]): Unit = {}
165+
def object_71(x: Array[_ <: AnyVal with Int]): Unit = {}
166+
167+
def stringARRAY_72(x: Array[String with Singleton]): Unit = {}
168+
def stringARRAY_73(x: Array[_ <: String]): Unit = {}
169+
def stringARRAY_74(x: Array[_ <: String with Singleton]): Unit = {}
170+
def stringARRAY_75(x: Array[_ <: Singleton with String]): Unit = {}
171+
def stringARRAY_76(x: Array[_ <: String with Any]): Unit = {}
172+
def stringARRAY_77(x: Array[_ <: Any with String]): Unit = {}
173+
def stringARRAY_78(x: Array[_ <: String with AnyRef]): Unit = {}
174+
def stringARRAY_79(x: Array[_ <: AnyRef with String]): Unit = {}
175+
176+
def object_80(x: Array[_ <: Singleton]): Unit = {}
177+
def object_81(x: Array[_ <: AnyVal]): Unit = {}
178+
def objectARRAY_82(x: Array[_ <: AnyRef]): Unit = {}
179+
def object_83(x: Array[_ <: Any]): Unit = {}
180+
181+
def object_84(x: Array[_ <: Serializable]): Unit = {}
182+
def object_85(x: Array[_ <: Univ]): Unit = {}
183+
def aARRAY_86(x: Array[_ <: A]): Unit = {}
184+
def aARRAY_87(x: Array[_ <: A with B]): Unit = {}
185+
186+
def objectARRAY_88(x: Array[Any]): Unit = {}
187+
def objectARRAY_89(x: Array[AnyRef]): Unit = {}
188+
def objectARRAY_90(x: Array[AnyVal]): Unit = {}
156189
}

tests/run/array-erasure.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,35 @@ object Test {
3535
}
3636
}
3737

38+
def arr3[T <: Int](x: Array[T]) = {
39+
x(0) == 2
40+
x.sameElements(x)
41+
}
42+
43+
def arr4[T <: Int | Double](x: Array[T]) = {
44+
x(0) == 2
45+
x.sameElements(x)
46+
}
47+
48+
def arr5[T <: Int | String](x: Array[T]) = {
49+
x(0) == 2
50+
x.sameElements(x)
51+
}
52+
53+
def arr6[T <: Matchable](x: Array[T]) = {
54+
x(0) == 2
55+
x.sameElements(x)
56+
}
57+
3858
def main(args: Array[String]): Unit = {
3959
val x: Array[Int] = Array(0)
4060

4161
arr0(x)
4262
arr1(x)
4363
arr2(x)
64+
arr3(x)
65+
arr4(x)
66+
arr5(x)
67+
arr6(x)
4468
}
4569
}

tests/run/arrays-from-java/A_1.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class A {
2+
def foo1[T <: Serializable](x: Array[T]): Unit = {}
3+
def foo2[T <: Object & Serializable](x: Array[T]): Unit = {}
4+
}

tests/run/arrays-from-java/C_2.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import java.io.Serializable;
2+
3+
class B extends A {
4+
@Override
5+
public <T extends Serializable> void foo1(T[] x) {}
6+
@Override
7+
public <T extends Object & Serializable> void foo2(T[] x) {}
8+
}
9+
10+
public class C_2 {
11+
public static void test() {
12+
A a = new A();
13+
B b = new B();
14+
String[] arr = { "" };
15+
a.foo1(arr);
16+
a.foo2(arr);
17+
18+
b.foo1(arr);
19+
b.foo2(arr);
20+
}
21+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
object Test {
2+
def main(args: Array[String]): Unit =
3+
C_2.test()
4+
}

0 commit comments

Comments
 (0)