Skip to content

Commit 91d174f

Browse files
committed
Array erasure: Better Java and Scala 2 compat
We sometimes need to erase `Array[T]` to `Object` instead of `Array[erasure(T)]` in method signatures, this happens when both primitive and reference arrays, or two different sort of primitive arrays could be passed as arguments because the lub of those types on the JVM is `Object`. But before this commit, we additionally erased Arrays whose element type is upper-bounded by a universal trait to Object (like `Array[_ <: Serializable]`), this isn't necessary since primitives do not extend those traits (except for compiler fictions like `Singleton`) and derived value classes in arrays are always boxed. Since having matching Scala 2 and 3 erasure is a lost cause (cf #11603), this commit align ourselves with Java which improves Java interop (because we can emit more accurate Java generic signatures) and should let us simplify erasure more in the future. It also turns out than even before this commit, we did not match Scala 2 erasure perfectly since we erase `Array[_ <: Int]` to `Array[Int]` whereas Scala 2 erases to `Object` (this is important since we want `IArray[Int]` to be erased to `Array[Int]`), so we need to special case Scala 2 array erasure anyway to handle this. This commit renames `isUnboundedGeneric` to `isGenericArrayElement` since the former was somewhat misleading, `T <: String | Any` is bounded, but `Array[T]` cannot be represented with a specific JVM array type.
1 parent f2e0a4c commit 91d174f

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)