Skip to content

Commit 28067e0

Browse files
committed
Union types: deduplication + Nothing type members absorption
Added the following simplification rules to OrType: - deduplication without subtype comparisons by the means of object equality/hash code - absorption of Nothing types (T | Nothing == T)
1 parent ef8cc3e commit 28067e0

File tree

6 files changed

+103
-14
lines changed

6 files changed

+103
-14
lines changed

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,18 @@ object TypeOps:
156156
tp.derivedAlias(simplify(tp.alias, theMap))
157157
case AndType(l, r) if !ctx.mode.is(Mode.Type) =>
158158
simplify(l, theMap) & simplify(r, theMap)
159-
case tp @ OrType(l, r)
160-
if !ctx.mode.is(Mode.Type)
161-
&& (tp.isSoft || l.isBottomType || r.isBottomType) =>
162-
// Normalize A | Null and Null | A to A even if the union is hard (i.e.
163-
// explicitly declared), but not if -Yexplicit-nulls is set. The reason is
164-
// that in this case the normal asSeenFrom machinery is not prepared to deal
165-
// with Nulls (which have no base classes). Under -Yexplicit-nulls, we take
166-
// corrective steps, so no widening is wanted.
167-
simplify(l, theMap) | simplify(r, theMap)
159+
case tp @ OrType(l, r) =>
160+
if !ctx.mode.is(Mode.Type) && (tp.isSoft || l.isBottomType || r.isBottomType) then
161+
// Normalize A | Null and Null | A to A even if the union is hard (i.e.
162+
// explicitly declared), but not if -Yexplicit-nulls is set. The reason is
163+
// that in this case the normal asSeenFrom machinery is not prepared to deal
164+
// with Nulls (which have no base classes). Under -Yexplicit-nulls, we take
165+
// corrective steps, so no widening is wanted.
166+
simplify(l, theMap) | simplify(r, theMap)
167+
else if r.isNothingType || (l eq r) then l
168+
else if l.isOrType || r.isOrType then tp.deduplicatedAbsorbingNothingTypes
169+
else if l.isNothingType then r
170+
else mapOver
168171
case tp @ CapturingType(parent, refs) =>
169172
if !ctx.mode.is(Mode.Type)
170173
&& refs.subCaptures(parent.captureSet, frozen = true).isOK

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ object Types {
446446
final def containsWildcardTypes(using Context) =
447447
existsPart(_.isInstanceOf[WildcardType], StopAt.Static, forceLazy = false)
448448

449+
/** Is this a union type? */
450+
final def isOrType: Boolean = this.isInstanceOf[OrType]
451+
449452
// ----- Higher-order combinators -----------------------------------
450453

451454
/** Returns true if there is a part of this type that satisfies predicate `p`.
@@ -3466,6 +3469,50 @@ object Types {
34663469
case that: OrType => tp1.eq(that.tp1) && tp2.eq(that.tp2) && isSoft == that.isSoft
34673470
case _ => false
34683471
}
3472+
3473+
/** Returns the set of non-union (leaf) types composing this union tree with Nothing types
3474+
* absorbed by other types, if present.
3475+
* For example:
3476+
* {{{A | B | C | B | (A & (B | C)) | Nothing}}}
3477+
* returns
3478+
* {{{Set(A, B, C, (A & (B | C)))}}}
3479+
*/
3480+
private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): Set[Type] = {
3481+
(tp1, tp2) match
3482+
case (l: OrType, r: OrType) =>
3483+
l.gatherTreeUniqueMembersAbsorbingNothingTypes ++ r.gatherTreeUniqueMembersAbsorbingNothingTypes
3484+
case (l: OrType, r) =>
3485+
if r.isNothingType
3486+
then l.gatherTreeUniqueMembersAbsorbingNothingTypes
3487+
else l.gatherTreeUniqueMembersAbsorbingNothingTypes + r
3488+
case (l, r: OrType) =>
3489+
if l.isNothingType
3490+
then r.gatherTreeUniqueMembersAbsorbingNothingTypes
3491+
else r.gatherTreeUniqueMembersAbsorbingNothingTypes + l
3492+
case (l, r) =>
3493+
if r.isNothingType then Set(l)
3494+
else if l.isNothingType then Set(r)
3495+
else Set(l, r)
3496+
}
3497+
3498+
/** Returns an equivalent union tree without repeated members and with Nothing types absorbed
3499+
* by other types, if present. Weaker than LUB.
3500+
*/
3501+
def deduplicatedAbsorbingNothingTypes(using Context): Type = {
3502+
val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes
3503+
val factorCount = orFactorCount(isSoft)
3504+
3505+
uniqueTreeMembers.size match {
3506+
case 1 =>
3507+
uniqueTreeMembers.head
3508+
case uniqueMembersCount if uniqueMembersCount < factorCount =>
3509+
val uniqueMembers = uniqueTreeMembers.iterator
3510+
val startingUnion = OrType(uniqueMembers.next(), uniqueMembers.next(), isSoft)
3511+
uniqueMembers.foldLeft(startingUnion)(OrType(_, _, isSoft))
3512+
case _ =>
3513+
this
3514+
}
3515+
}
34693516
}
34703517

34713518
final class CachedOrType(tp1: Type, tp2: Type, override val isSoft: Boolean) extends OrType(tp1, tp2)

tests/neg/i11694.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ def test1 = {
55
def f21: (Int => Int) | Null = x => x + 1
66
def f22: Null | (Int => Int) = x => x + 1
77

8-
def f31: (Int => Int) | (Int => Int) = x => x + 1 // error
9-
def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1 // error
8+
def f31: (Int => Int) | (Int => Int) = x => x + 1
9+
def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1
1010

1111
def f41: (Int => Int) & (Int => Int) = x => x + 1
1212
def f42: (Int => Int) & (Int => Int) & Any = x => x + 1

tests/neg/i14823a.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ object Foo {
1313
case class A[T]() extends Foo[T]
1414
}
1515

16-
val foo = summon[Mirror.Of[Box[Int] | Box[Int]]] // error
17-
val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[Int]]] // error
18-
def baz = summon[deriving.Mirror.Of[Foo[String] | Foo[String]]] // error
16+
val foo = summon[Mirror.Of[Box[Int] | Box[String]]] // error
17+
val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[String]]] // error
18+
def baz = summon[deriving.Mirror.Of[Foo[Int] | Foo[String]]] // error
1919

2020
def qux = summon[deriving.Mirror.Of[Option[Int] | Option[String]]] // error

tests/printing/i10693.check

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[[syntax trees at end of typer]] // tests/printing/i10693.scala
2+
package <empty> {
3+
final lazy module val i10693$package: i10693$package = new i10693$package()
4+
final module class i10693$package() extends Object() {
5+
this: i10693$package.type =>
6+
def test[A >: Nothing <: Any, B >: Nothing <: Any](a: A, b: B): A | B = a
7+
val v0: String | Int = test[String, Int]("string", 1)
8+
val v1: Int | String = test[Int, String](1, "string")
9+
val v2: String | Int = test[(String | Int), (Int | String)](v0, v1)
10+
val v3: Int | String = test[(Int | String), (String | Int)](v1, v0)
11+
val v4: String | Int = test[(String | Int), (Int | String)](v2, v3)
12+
val v5: Int | String = test[(Int | String), (String | Int)](v3, v2)
13+
val v6: String | Int = test[(String | Int), (Int | String)](v4, v5)
14+
val t0: List[Int] = Tuple1.apply[Int](1).toList
15+
val t1: List[Int] = Tuple2.apply[Int, Int](1, 2).toList
16+
val t2: List[Int] = Tuple3.apply[Int, Int, Int](1, 2, 3).toList
17+
val t3: List[Int | String] =
18+
Tuple3.apply[String, Int, Int]("A", 2, 3).toList
19+
val t4: List[String | Int] =
20+
Tuple3.apply[Int, String, String](1, "B", "C").toList
21+
}
22+
}
23+

tests/printing/i10693.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// minimized code
2+
def test[A, B](a: A, b: B): A | B = a
3+
val v0 = test("string", 1)
4+
val v1 = test(1, "string")
5+
val v2 = test(v0, v1)
6+
val v3 = test(v1, v0)
7+
val v4 = test(v2, v3)
8+
val v5 = test(v3, v2)
9+
val v6 = test(v4, v5)
10+
11+
// issue comments section examples
12+
val t0 = Tuple1(1).toList
13+
val t1 = (1, 2).toList
14+
val t2 = (1, 2, 3).toList
15+
val t3 = ("A", 2, 3).toList
16+
val t4 = (1, "B", "C").toList

0 commit comments

Comments
 (0)