Skip to content

Commit 1e74e9d

Browse files
authored
Merge pull request #5926 from abeln/erase-bottom
Fix #5823: better erasure of bottom types
2 parents aa715fe + 909117b commit 1e74e9d

File tree

4 files changed

+162
-48
lines changed

4 files changed

+162
-48
lines changed

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

Lines changed: 56 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -248,54 +248,62 @@ object TypeErasure {
248248
* The reason to pick last is that we prefer classes over traits that way,
249249
* which leads to more predictable bytecode and (?) faster dynamic dispatch.
250250
*/
251-
def erasedLub(tp1: Type, tp2: Type)(implicit ctx: Context): Type = tp1 match {
252-
case JavaArrayType(elem1) =>
253-
import dotty.tools.dotc.transform.TypeUtils._
254-
tp2 match {
255-
case JavaArrayType(elem2) =>
256-
if (elem1.isPrimitiveValueType || elem2.isPrimitiveValueType) {
257-
if (elem1.classSymbol eq elem2.classSymbol) // same primitive
258-
JavaArrayType(elem1)
259-
else defn.ObjectType
260-
} else JavaArrayType(erasedLub(elem1, elem2))
261-
case _ => defn.ObjectType
262-
}
263-
case _ =>
264-
tp2 match {
265-
case JavaArrayType(_) => defn.ObjectType
266-
case _ =>
267-
val cls2 = tp2.classSymbol
268-
269-
/** takeWhile+1 */
270-
def takeUntil[T](l: List[T])(f: T => Boolean): List[T] = {
271-
@tailrec def loop(tail: List[T], acc: List[T]): List[T] =
272-
tail match {
273-
case h :: t => loop(if (f(h)) t else Nil, h :: acc)
274-
case Nil => acc.reverse
275-
}
276-
loop(l, Nil)
277-
}
278-
279-
// We are not interested in anything that is not a supertype of tp2
280-
val tp2superclasses = tp1.baseClasses.filter(cls2.derivesFrom)
281-
282-
// From the spec, "Linearization also satisfies the property that a
283-
// linearization of a class always contains the linearization of its
284-
// direct superclass as a suffix"; it's enough to consider every
285-
// candidate up to the first class.
286-
val candidates = takeUntil(tp2superclasses)(!_.is(Trait))
287-
288-
// Candidates st "no other common superclass or trait derives from S"
289-
val minimums = candidates.filter { cand =>
290-
candidates.forall(x => !x.derivesFrom(cand) || x.eq(cand))
291-
}
292-
293-
// Pick the last minimum to prioritise classes over traits
294-
minimums.lastOption match {
295-
case Some(lub) => valueErasure(lub.typeRef)
296-
case _ => defn.ObjectType
297-
}
298-
}
251+
def erasedLub(tp1: Type, tp2: Type)(implicit ctx: Context): Type = {
252+
// After erasure, C | {Null, Nothing} is just C, if C is a reference type.
253+
// We need to short-circuit this case here because the regular lub logic below
254+
// relies on the class hierarchy, which doesn't properly capture `Null`s subtyping
255+
// behaviour.
256+
if (defn.isBottomType(tp1) && tp2.derivesFrom(defn.ObjectClass)) return tp2
257+
if (defn.isBottomType(tp2) && tp1.derivesFrom(defn.ObjectClass)) return tp1
258+
tp1 match {
259+
case JavaArrayType(elem1) =>
260+
import dotty.tools.dotc.transform.TypeUtils._
261+
tp2 match {
262+
case JavaArrayType(elem2) =>
263+
if (elem1.isPrimitiveValueType || elem2.isPrimitiveValueType) {
264+
if (elem1.classSymbol eq elem2.classSymbol) // same primitive
265+
JavaArrayType(elem1)
266+
else defn.ObjectType
267+
} else JavaArrayType(erasedLub(elem1, elem2))
268+
case _ => defn.ObjectType
269+
}
270+
case _ =>
271+
tp2 match {
272+
case JavaArrayType(_) => defn.ObjectType
273+
case _ =>
274+
val cls2 = tp2.classSymbol
275+
276+
/** takeWhile+1 */
277+
def takeUntil[T](l: List[T])(f: T => Boolean): List[T] = {
278+
@tailrec def loop(tail: List[T], acc: List[T]): List[T] =
279+
tail match {
280+
case h :: t => loop(if (f(h)) t else Nil, h :: acc)
281+
case Nil => acc.reverse
282+
}
283+
loop(l, Nil)
284+
}
285+
286+
// We are not interested in anything that is not a supertype of tp2
287+
val tp2superclasses = tp1.baseClasses.filter(cls2.derivesFrom)
288+
289+
// From the spec, "Linearization also satisfies the property that a
290+
// linearization of a class always contains the linearization of its
291+
// direct superclass as a suffix"; it's enough to consider every
292+
// candidate up to the first class.
293+
val candidates = takeUntil(tp2superclasses)(!_.is(Trait))
294+
295+
// Candidates st "no other common superclass or trait derives from S"
296+
val minimums = candidates.filter { cand =>
297+
candidates.forall(x => !x.derivesFrom(cand) || x.eq(cand))
298+
}
299+
300+
// Pick the last minimum to prioritise classes over traits
301+
minimums.lastOption match {
302+
case Some(lub) => valueErasure(lub.typeRef)
303+
case _ => defn.ObjectType
304+
}
305+
}
306+
}
299307
}
300308

301309
/** The erased greatest lower bound of two erased type picks one of the two argument types.

tests/neg/i5823.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Test that `C|Null` is erased to `C` if `C` is
2+
// a reference type.
3+
// If `C` is a value type, then `C|Null = Object`.
4+
// Ditto for `C|Nothing`.
5+
6+
class A
7+
class B
8+
9+
class Foo {
10+
11+
// ok, because A and B are <: Object.
12+
def foo(a: A|Null): Unit = ()
13+
def foo(b: B|Null): Unit = ()
14+
15+
def bar(a: Int|Null): Unit = ()
16+
def bar(b: Boolean|Null): Unit = () // error: signatures match
17+
18+
// ok, T is erased to `String` and `Integer`, respectively
19+
def gen[T <: String](s: T|Null): Unit = ()
20+
def gen[T <: Integer](i: T|Null): Unit = ()
21+
22+
def gen2[T <: Int](i: T|Null): Unit = ()
23+
def gen2[T <: Boolean](b: T|Null): Unit = () // error: signatures match
24+
25+
// ok, because A and B are <: Object.
26+
def foo2(a: A|Nothing): Unit = ()
27+
def foo2(b: B|Nothing): Unit = ()
28+
29+
def bar2(a: Int|Nothing): Unit = ()
30+
def bar2(b: Boolean|Nothing): Unit = () // error: signatures match
31+
32+
// ok, T is erased to `String` and `Integer`, respectively
33+
def gen3[T <: String](s: T|Nothing): Unit = ()
34+
def gen3[T <: Integer](i: T|Nothing): Unit = ()
35+
36+
def gen4[T <: Int](i: T|Nothing): Unit = ()
37+
def gen4[T <: Boolean](b: T|Nothing): Unit = () // error: signatures match
38+
}

tests/run/i5823.check

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
foo(A) called
2+
foo(B) called
3+
foo(C) called
4+
foo(D) called
5+
bar(A) called
6+
bar(B) called
7+
fooz(A) called
8+
fooz(B) called

tests/run/i5823.scala

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Test that `C|Null` and `C|Nothing` are erased to `C`.
2+
3+
class A
4+
class B
5+
class C
6+
class D
7+
8+
object Foo {
9+
// This code would not have compiled before, when `C|Null` was erased
10+
// to `Object`, because post-erasure we would end up with multiple methods
11+
// with the same signature.
12+
13+
def foo(a: A|Null): Unit = {
14+
println("foo(A) called")
15+
}
16+
17+
def foo(b: B|Null): Unit = {
18+
println("foo(B) called")
19+
}
20+
21+
def foo(c: Null|C): Unit = {
22+
println("foo(C) called")
23+
}
24+
25+
def foo(d: Null|D): Unit = {
26+
println("foo(D) called")
27+
}
28+
29+
def bar[T <: A](a: Null|T): Unit = {
30+
println("bar(A) called")
31+
}
32+
33+
def bar[T <: B](b: Null|T): Unit = {
34+
println("bar(B) called")
35+
}
36+
37+
def fooz(a: A|Nothing): Unit = {
38+
println("fooz(A) called")
39+
}
40+
41+
def fooz(b: B|Nothing): Unit = {
42+
println("fooz(B) called")
43+
}
44+
}
45+
46+
object Test {
47+
def main(args: Array[String]): Unit = {
48+
import Foo._
49+
foo(new A)
50+
foo(new B)
51+
foo(new C)
52+
foo(new D)
53+
54+
bar(new A)
55+
bar(new B)
56+
57+
fooz(new A)
58+
fooz(new B)
59+
}
60+
}

0 commit comments

Comments
 (0)