Skip to content

Commit 9cd8ce8

Browse files
committed
Erase T | Nothing signatures types into T
This make the erasure different for value types as they now correctly erase to the primitive types instead of `Object`. Aligns | Nothing with & Any and Nothing | with Any & for value types. This also fixes `summon[ClassTag[Int | Nothing]]` as it is now equivalent to `summon[ClassTag[Int]]`. Fixes #14970 Fixes #14964 > ⚠️ This is a binary breaking change. > the Previous version generated the `def f: Int` and `def f: Object` variants of the method. > In the new one, we only generate the `def f: Int` version. > Unfortunately, the previous version called the boxed version of the interface that does not exist anymore. > > Is this something that we encounter in practice?
1 parent 7ee8986 commit 9cd8ce8

File tree

7 files changed

+54
-11
lines changed

7 files changed

+54
-11
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -363,13 +363,13 @@ object TypeErasure {
363363
* which leads to more predictable bytecode and (?) faster dynamic dispatch.
364364
*/
365365
def erasedLub(tp1: Type, tp2: Type)(using Context): Type = {
366-
// After erasure, C | {Null, Nothing} is just C, if C is a reference type.
367-
// We need to short-circuit this case here because the regular lub logic below
368-
// relies on the class hierarchy, which doesn't properly capture `Null`s subtyping
369-
// behaviour.
370-
if (tp1.isBottomTypeAfterErasure && tp2.derivesFrom(defn.ObjectClass)) return tp2
371-
if (tp2.isBottomTypeAfterErasure && tp1.derivesFrom(defn.ObjectClass)) return tp1
372-
tp1 match {
366+
// We need to short-circuit the following 2 case because the regular lub logic in the else relies on
367+
// the class hierarchy, which doesn't properly capture `Nothing`/`Null` subtyping behaviour.
368+
if tp1.isRef(defn.NothingClass) || (tp1.isRef(defn.NullClass) && tp2.derivesFrom(defn.ObjectClass)) then
369+
tp2 // After erasure, Nothing | T is just T and Null | C is just C, if C is a reference type.
370+
else if tp2.isRef(defn.NothingClass) || (tp2.isRef(defn.NullClass) && tp1.derivesFrom(defn.ObjectClass)) then
371+
tp1 // After erasure, T | Nothing is just T and C | Null is just C, if C is a reference type.
372+
else tp1 match {
373373
case JavaArrayType(elem1) =>
374374
import dotty.tools.dotc.transform.TypeUtils._
375375
tp2 match {

compiler/test/dotty/tools/backend/jvm/DottyBytecodeTests.scala

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import scala.tools.asm.Opcodes
1313
import scala.jdk.CollectionConverters._
1414
import Opcodes._
1515

16-
class TestBCode extends DottyBytecodeTest {
16+
class DottyBytecodeTests extends DottyBytecodeTest {
1717
import ASMConverters._
1818
@Test def nullChecks = {
1919
val source = """
@@ -1234,6 +1234,27 @@ class TestBCode extends DottyBytecodeTest {
12341234
Label(9), Op(IRETURN)))
12351235
}
12361236
}
1237+
1238+
/** Check that erasure if `Int | Nothing` is `int` */
1239+
@Test def i14970 = {
1240+
val source =
1241+
s"""class Foo {
1242+
| def foo: Int | Nothing = 1
1243+
| def bar: Nothing | Int = 1
1244+
|}
1245+
""".stripMargin
1246+
1247+
checkBCode(source) { dir =>
1248+
val clsIn = dir.lookupName("Foo.class", directory = false).input
1249+
val clsNode = loadClassNode(clsIn)
1250+
def testSig(methodName: String, expectedSignature: String) = {
1251+
val signature = clsNode.methods.asScala.filter(_.name == methodName).map(_.signature)
1252+
assertEquals(List(expectedSignature), signature)
1253+
}
1254+
testSig("foo", "()I")
1255+
testSig("bar", "()I")
1256+
}
1257+
}
12371258
}
12381259

12391260
object invocationReceiversTestCode {

tests/neg/i5823.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class A
77
class B
88

99
class Foo {
10-
10+
1111
// ok, because A and B are <: Object.
1212
def foo(a: A|Null): Unit = ()
1313
def foo(b: B|Null): Unit = ()
@@ -26,13 +26,15 @@ class Foo {
2626
def foo2(a: A|Nothing): Unit = ()
2727
def foo2(b: B|Nothing): Unit = ()
2828

29+
// ok because erased to primitive types
2930
def bar2(a: Int|Nothing): Unit = ()
30-
def bar2(b: Boolean|Nothing): Unit = () // error: signatures match
31+
def bar2(b: Boolean|Nothing): Unit = ()
3132

3233
// ok, T is erased to `String` and `Integer`, respectively
3334
def gen3[T <: String](s: T|Nothing): Unit = ()
3435
def gen3[T <: Integer](i: T|Nothing): Unit = ()
3536

37+
// ok because erased to primitive types
3638
def gen4[T <: Int](i: T|Nothing): Unit = ()
37-
def gen4[T <: Boolean](b: T|Nothing): Unit = () // error: signatures match
39+
def gen4[T <: Boolean](b: T|Nothing): Unit = ()
3840
}

tests/run/i14964.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@main def Test: Unit =
2+
val xs = (1, 2).toList
3+
val a1 = xs.toArray
4+
println((1, 2).toList.toArray)
5+
val a2 = (1, 2).toList.toArray

tests/run/i14964b.check

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Int
2+
Int
3+
Int
4+
Int

tests/run/i14964b.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@main def Test: Unit =
2+
println(summon[reflect.ClassTag[Int]]) // classOf[Int]
3+
println(summon[reflect.ClassTag[Int | Int]]) // classOf[Int]
4+
println(summon[reflect.ClassTag[Int | 1]]) // classOf[Int]
5+
println(summon[reflect.ClassTag[Int | Nothing]]) // classOf[Int]

tests/run/i14970.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object Test {
2+
def main(args: Array[String]): Unit = {
3+
val x: Array[Int | Nothing] = Array()
4+
val y: Array[Int] = x
5+
}
6+
}

0 commit comments

Comments
 (0)