From 2b6cb1fb1cb4b0b8cfeb1f80fc4b9b1ce932dd57 Mon Sep 17 00:00:00 2001 From: mucciolo Date: Wed, 9 Nov 2022 20:05:14 -0300 Subject: [PATCH 01/13] 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) --- .../src/dotty/tools/dotc/core/TypeOps.scala | 21 +++++---- .../src/dotty/tools/dotc/core/Types.scala | 47 +++++++++++++++++++ tests/neg/i11694.scala | 4 +- tests/neg/i14823a.scala | 6 +-- tests/printing/i10693.check | 23 +++++++++ tests/printing/i10693.scala | 16 +++++++ 6 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 tests/printing/i10693.check create mode 100644 tests/printing/i10693.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index 6809e4b9083c..f2bd3f6a75a3 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -157,15 +157,18 @@ object TypeOps: tp.derivedAlias(simplify(tp.alias, theMap)) case AndType(l, r) if !ctx.mode.is(Mode.Type) => simplify(l, theMap) & simplify(r, theMap) - case tp @ OrType(l, r) - if !ctx.mode.is(Mode.Type) - && (tp.isSoft || l.isBottomType || r.isBottomType) => - // Normalize A | Null and Null | A to A even if the union is hard (i.e. - // explicitly declared), but not if -Yexplicit-nulls is set. The reason is - // that in this case the normal asSeenFrom machinery is not prepared to deal - // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take - // corrective steps, so no widening is wanted. - simplify(l, theMap) | simplify(r, theMap) + case tp @ OrType(l, r) => + if !ctx.mode.is(Mode.Type) && (tp.isSoft || l.isBottomType || r.isBottomType) then + // Normalize A | Null and Null | A to A even if the union is hard (i.e. + // explicitly declared), but not if -Yexplicit-nulls is set. The reason is + // that in this case the normal asSeenFrom machinery is not prepared to deal + // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take + // corrective steps, so no widening is wanted. + simplify(l, theMap) | simplify(r, theMap) + else if r.isNothingType || (l eq r) then l + else if l.isOrType || r.isOrType then tp.deduplicatedAbsorbingNothingTypes + else if l.isNothingType then r + else mapOver case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) && refs.subCaptures(parent.captureSet, frozen = true).isOK diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index f000fe53f239..72fc1f61a43b 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -446,6 +446,9 @@ object Types { final def containsWildcardTypes(using Context) = existsPart(_.isInstanceOf[WildcardType], StopAt.Static, forceLazy = false) + /** Is this a union type? */ + final def isOrType: Boolean = this.isInstanceOf[OrType] + // ----- Higher-order combinators ----------------------------------- /** Returns true if there is a part of this type that satisfies predicate `p`. @@ -3482,6 +3485,50 @@ object Types { case that: OrType => tp1.eq(that.tp1) && tp2.eq(that.tp2) && isSoft == that.isSoft case _ => false } + + /** Returns the set of non-union (leaf) types composing this union tree with Nothing types + * absorbed by other types, if present. + * For example: + * {{{A | B | C | B | (A & (B | C)) | Nothing}}} + * returns + * {{{Set(A, B, C, (A & (B | C)))}}} + */ + private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): Set[Type] = { + (tp1, tp2) match + case (l: OrType, r: OrType) => + l.gatherTreeUniqueMembersAbsorbingNothingTypes ++ r.gatherTreeUniqueMembersAbsorbingNothingTypes + case (l: OrType, r) => + if r.isNothingType + then l.gatherTreeUniqueMembersAbsorbingNothingTypes + else l.gatherTreeUniqueMembersAbsorbingNothingTypes + r + case (l, r: OrType) => + if l.isNothingType + then r.gatherTreeUniqueMembersAbsorbingNothingTypes + else r.gatherTreeUniqueMembersAbsorbingNothingTypes + l + case (l, r) => + if r.isNothingType then Set(l) + else if l.isNothingType then Set(r) + else Set(l, r) + } + + /** Returns an equivalent union tree without repeated members and with Nothing types absorbed + * by other types, if present. Weaker than LUB. + */ + def deduplicatedAbsorbingNothingTypes(using Context): Type = { + val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes + val factorCount = orFactorCount(isSoft) + + uniqueTreeMembers.size match { + case 1 => + uniqueTreeMembers.head + case uniqueMembersCount if uniqueMembersCount < factorCount => + val uniqueMembers = uniqueTreeMembers.iterator + val startingUnion = OrType(uniqueMembers.next(), uniqueMembers.next(), isSoft) + uniqueMembers.foldLeft(startingUnion)(OrType(_, _, isSoft)) + case _ => + this + } + } } final class CachedOrType(tp1: Type, tp2: Type, override val isSoft: Boolean) extends OrType(tp1, tp2) diff --git a/tests/neg/i11694.scala b/tests/neg/i11694.scala index d43fe4aa99e3..67138fd5a7eb 100644 --- a/tests/neg/i11694.scala +++ b/tests/neg/i11694.scala @@ -5,8 +5,8 @@ def test1 = { def f21: (Int => Int) | Null = x => x + 1 def f22: Null | (Int => Int) = x => x + 1 - def f31: (Int => Int) | (Int => Int) = x => x + 1 // error - def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1 // error + def f31: (Int => Int) | (Int => Int) = x => x + 1 + def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1 def f41: (Int => Int) & (Int => Int) = x => x + 1 def f42: (Int => Int) & (Int => Int) & Any = x => x + 1 diff --git a/tests/neg/i14823a.scala b/tests/neg/i14823a.scala index b80f8e5062e3..851f9fa5e3d0 100644 --- a/tests/neg/i14823a.scala +++ b/tests/neg/i14823a.scala @@ -13,8 +13,8 @@ object Foo { case class A[T]() extends Foo[T] } -val foo = summon[Mirror.Of[Box[Int] | Box[Int]]] // error -val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[Int]]] // error -def baz = summon[deriving.Mirror.Of[Foo[String] | Foo[String]]] // error +val foo = summon[Mirror.Of[Box[Int] | Box[String]]] // error +val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[String]]] // error +def baz = summon[deriving.Mirror.Of[Foo[Int] | Foo[String]]] // error def qux = summon[deriving.Mirror.Of[Option[Int] | Option[String]]] // error diff --git a/tests/printing/i10693.check b/tests/printing/i10693.check new file mode 100644 index 000000000000..becc93dbbca3 --- /dev/null +++ b/tests/printing/i10693.check @@ -0,0 +1,23 @@ +[[syntax trees at end of typer]] // tests/printing/i10693.scala +package { + final lazy module val i10693$package: i10693$package = new i10693$package() + final module class i10693$package() extends Object() { + this: i10693$package.type => + def test[A >: Nothing <: Any, B >: Nothing <: Any](a: A, b: B): A | B = a + val v0: String | Int = test[String, Int]("string", 1) + val v1: Int | String = test[Int, String](1, "string") + val v2: String | Int = test[(String | Int), (Int | String)](v0, v1) + val v3: Int | String = test[(Int | String), (String | Int)](v1, v0) + val v4: String | Int = test[(String | Int), (Int | String)](v2, v3) + val v5: Int | String = test[(Int | String), (String | Int)](v3, v2) + val v6: String | Int = test[(String | Int), (Int | String)](v4, v5) + val t0: List[Int] = Tuple1.apply[Int](1).toList + val t1: List[Int] = Tuple2.apply[Int, Int](1, 2).toList + val t2: List[Int] = Tuple3.apply[Int, Int, Int](1, 2, 3).toList + val t3: List[Int | String] = + Tuple3.apply[String, Int, Int]("A", 2, 3).toList + val t4: List[String | Int] = + Tuple3.apply[Int, String, String](1, "B", "C").toList + } +} + diff --git a/tests/printing/i10693.scala b/tests/printing/i10693.scala new file mode 100644 index 000000000000..0f852f70c3fd --- /dev/null +++ b/tests/printing/i10693.scala @@ -0,0 +1,16 @@ +// minimized code +def test[A, B](a: A, b: B): A | B = a +val v0 = test("string", 1) +val v1 = test(1, "string") +val v2 = test(v0, v1) +val v3 = test(v1, v0) +val v4 = test(v2, v3) +val v5 = test(v3, v2) +val v6 = test(v4, v5) + +// issue comments section examples +val t0 = Tuple1(1).toList +val t1 = (1, 2).toList +val t2 = (1, 2, 3).toList +val t3 = ("A", 2, 3).toList +val t4 = (1, "B", "C").toList From b5ce8974355a686b7ecb54f1caf40d6a203a4b41 Mon Sep 17 00:00:00 2001 From: mucciolo Date: Thu, 10 Nov 2022 13:21:07 -0300 Subject: [PATCH 02/13] Applied review comments Updated test check files and Markdown syntax used in Scaladoc --- compiler/src/dotty/tools/dotc/core/Types.scala | 7 ++----- tests/printing/i10693.check | 16 ++++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 72fc1f61a43b..dbe61ffba94b 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3487,11 +3487,8 @@ object Types { } /** Returns the set of non-union (leaf) types composing this union tree with Nothing types - * absorbed by other types, if present. - * For example: - * {{{A | B | C | B | (A & (B | C)) | Nothing}}} - * returns - * {{{Set(A, B, C, (A & (B | C)))}}} + * absorbed by other types, if present. For example:
+ * `(A | B | C | B | (A & (B | C)) | Nothing)` returns `Set(A, B, C, (A & (B | C)))`. */ private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): Set[Type] = { (tp1, tp2) match diff --git a/tests/printing/i10693.check b/tests/printing/i10693.check index becc93dbbca3..4c3842d75975 100644 --- a/tests/printing/i10693.check +++ b/tests/printing/i10693.check @@ -1,22 +1,22 @@ [[syntax trees at end of typer]] // tests/printing/i10693.scala package { final lazy module val i10693$package: i10693$package = new i10693$package() - final module class i10693$package() extends Object() { + final module class i10693$package() extends Object() { this: i10693$package.type => def test[A >: Nothing <: Any, B >: Nothing <: Any](a: A, b: B): A | B = a val v0: String | Int = test[String, Int]("string", 1) val v1: Int | String = test[Int, String](1, "string") - val v2: String | Int = test[(String | Int), (Int | String)](v0, v1) - val v3: Int | String = test[(Int | String), (String | Int)](v1, v0) - val v4: String | Int = test[(String | Int), (Int | String)](v2, v3) - val v5: Int | String = test[(Int | String), (String | Int)](v3, v2) - val v6: String | Int = test[(String | Int), (Int | String)](v4, v5) + val v2: String | Int = test[String | Int, Int | String](v0, v1) + val v3: Int | String = test[Int | String, String | Int](v1, v0) + val v4: String | Int = test[String | Int, Int | String](v2, v3) + val v5: Int | String = test[Int | String, String | Int](v3, v2) + val v6: String | Int = test[String | Int, Int | String](v4, v5) val t0: List[Int] = Tuple1.apply[Int](1).toList val t1: List[Int] = Tuple2.apply[Int, Int](1, 2).toList val t2: List[Int] = Tuple3.apply[Int, Int, Int](1, 2, 3).toList - val t3: List[Int | String] = + val t3: List[Int | String] = Tuple3.apply[String, Int, Int]("A", 2, 3).toList - val t4: List[String | Int] = + val t4: List[String | Int] = Tuple3.apply[Int, String, String](1, "B", "C").toList } } From 2fa6ddcd0e467a7a81ac2aedb7e97f5c2a1c1054 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Tue, 22 Nov 2022 11:51:54 -0300 Subject: [PATCH 03/13] Inlined Type.isOrType --- compiler/src/dotty/tools/dotc/core/TypeOps.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index f2bd3f6a75a3..d99fdc636f23 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -166,7 +166,7 @@ object TypeOps: // corrective steps, so no widening is wanted. simplify(l, theMap) | simplify(r, theMap) else if r.isNothingType || (l eq r) then l - else if l.isOrType || r.isOrType then tp.deduplicatedAbsorbingNothingTypes + else if l.isInstanceOf[OrType] || r.isInstanceOf[OrType] then tp.deduplicatedAbsorbingNothingTypes else if l.isNothingType then r else mapOver case tp @ CapturingType(parent, refs) => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index dbe61ffba94b..ba9924499472 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -446,9 +446,6 @@ object Types { final def containsWildcardTypes(using Context) = existsPart(_.isInstanceOf[WildcardType], StopAt.Static, forceLazy = false) - /** Is this a union type? */ - final def isOrType: Boolean = this.isInstanceOf[OrType] - // ----- Higher-order combinators ----------------------------------- /** Returns true if there is a part of this type that satisfies predicate `p`. From 28f855d6b724041925924d0912a725ab5c36bb5a Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 25 Nov 2022 19:43:09 -0300 Subject: [PATCH 04/13] Added EqLinkedHashSet, an identity-based linked hash set --- .../tools/dotc/util/EqLinkedHashSet.scala | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala diff --git a/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala new file mode 100644 index 000000000000..73124d106ca6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala @@ -0,0 +1,46 @@ +package dotty.tools.dotc.util + +import scala.collection.mutable.ArrayBuffer + +class EqLinkedHashSet[T]( + initialCapacity: Int = 8, capacityMultiple: Int = 2 +) extends MutableSet[T] { + + private val map: MutableMap[T, Unit] = new EqHashMap(initialCapacity, capacityMultiple) + private val linkingArray: ArrayBuffer[T] = new ArrayBuffer(initialCapacity) + + override def +=(x: T): Unit = + map.update(x, ()) + if map.size != linkingArray.size then linkingArray += x + + override def put(x: T): T = + this += x + x + + override def -=(x: T): Unit = + map -= x + if map.size != linkingArray.size then linkingArray -= x + + override def clear(): Unit = + map.clear() + linkingArray.clear() + + override def lookup(x: T): T | Null = if map.contains(x) then x else null + + override def size: Int = map.size + + override def iterator: Iterator[T] = linkingArray.iterator + +} + +object EqLinkedHashSet { + def apply[T](x: T): EqLinkedHashSet[T] = + val set = new EqLinkedHashSet[T] + set += x + set + + def apply[T](x: T, y: T): EqLinkedHashSet[T] = + val set = EqLinkedHashSet(x) + set += y + set +} From a555606f7bac316ddab6a64b4a141f857848ac70 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 25 Nov 2022 19:47:21 -0300 Subject: [PATCH 05/13] Optimized Types.gatherTreeUniqueMembersAbsorbingNothingTypes - Replaced recursion by a while loop - Replaced immutable.Set by EqLinkedHashSet --- .../src/dotty/tools/dotc/core/Types.scala | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index ba9924499472..c80e471c0da0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -38,6 +38,8 @@ import java.lang.ref.WeakReference import compiletime.uninitialized import cc.{CapturingType, CaptureSet, derivedCapturingType, isBoxedCapturing, EventuallyCapturingType, boxedUnlessFun} import CaptureSet.{CompareResult, IdempotentCaptRefMap, IdentityCaptRefMap} +import scala.collection.mutable.ListBuffer +import dotty.tools.dotc.util._ import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -3485,40 +3487,52 @@ object Types { /** Returns the set of non-union (leaf) types composing this union tree with Nothing types * absorbed by other types, if present. For example:
- * `(A | B | C | B | (A & (B | C)) | Nothing)` returns `Set(A, B, C, (A & (B | C)))`. - */ - private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): Set[Type] = { - (tp1, tp2) match - case (l: OrType, r: OrType) => - l.gatherTreeUniqueMembersAbsorbingNothingTypes ++ r.gatherTreeUniqueMembersAbsorbingNothingTypes - case (l: OrType, r) => - if r.isNothingType - then l.gatherTreeUniqueMembersAbsorbingNothingTypes - else l.gatherTreeUniqueMembersAbsorbingNothingTypes + r - case (l, r: OrType) => - if l.isNothingType - then r.gatherTreeUniqueMembersAbsorbingNothingTypes - else r.gatherTreeUniqueMembersAbsorbingNothingTypes + l - case (l, r) => - if r.isNothingType then Set(l) - else if l.isNothingType then Set(r) - else Set(l, r) - } - - /** Returns an equivalent union tree without repeated members and with Nothing types absorbed - * by other types, if present. Weaker than LUB. + * `(A | B | C | B | (A & (B | C)) | Nothing)` returns `{A, B, C, (A & (B | C))}`. + */ + private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): MutableSet[Type] = { + + var trees = List(this) + val uniqueTreeMembers = new EqLinkedHashSet[Type] + + while (trees.nonEmpty) { + trees match { + case head :: tail => + head match { + case OrType(l: OrType, r: OrType) => + trees = l :: r :: tail + case OrType(l, r: OrType) => + trees = r :: tail + if !l.isNothingType then uniqueTreeMembers += l + case OrType(l: OrType, r) => + trees = l :: tail + if !r.isNothingType then uniqueTreeMembers += r + case OrType(l, r) => + trees = tail + uniqueTreeMembers += l + if !r.isNothingType then uniqueTreeMembers += r + } + case _ => + } + } + + uniqueTreeMembers + } + + /** Returns an equivalent union tree without repeated members, preserving order and absorbing + * Nothing types, if present. Weaker than LUB. */ def deduplicatedAbsorbingNothingTypes(using Context): Type = { + val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes val factorCount = orFactorCount(isSoft) uniqueTreeMembers.size match { case 1 => - uniqueTreeMembers.head + uniqueTreeMembers.iterator.next() case uniqueMembersCount if uniqueMembersCount < factorCount => - val uniqueMembers = uniqueTreeMembers.iterator - val startingUnion = OrType(uniqueMembers.next(), uniqueMembers.next(), isSoft) - uniqueMembers.foldLeft(startingUnion)(OrType(_, _, isSoft)) + val members = uniqueTreeMembers.iterator + val startingUnion = OrType(members.next(), members.next(), isSoft) + members.foldLeft(startingUnion)(OrType(_, _, isSoft)) case _ => this } From 655dc3134c55819b3e5a9ebcc1daa343f3794234 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 25 Nov 2022 19:50:32 -0300 Subject: [PATCH 06/13] Reverted i11694 and i14823a --- tests/neg/i11694.scala | 4 ++-- tests/neg/i14823a.scala | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/neg/i11694.scala b/tests/neg/i11694.scala index 67138fd5a7eb..d43fe4aa99e3 100644 --- a/tests/neg/i11694.scala +++ b/tests/neg/i11694.scala @@ -5,8 +5,8 @@ def test1 = { def f21: (Int => Int) | Null = x => x + 1 def f22: Null | (Int => Int) = x => x + 1 - def f31: (Int => Int) | (Int => Int) = x => x + 1 - def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1 + def f31: (Int => Int) | (Int => Int) = x => x + 1 // error + def f32: (Int => Int) | (Int => Int) | Unit = x => x + 1 // error def f41: (Int => Int) & (Int => Int) = x => x + 1 def f42: (Int => Int) & (Int => Int) & Any = x => x + 1 diff --git a/tests/neg/i14823a.scala b/tests/neg/i14823a.scala index 851f9fa5e3d0..b80f8e5062e3 100644 --- a/tests/neg/i14823a.scala +++ b/tests/neg/i14823a.scala @@ -13,8 +13,8 @@ object Foo { case class A[T]() extends Foo[T] } -val foo = summon[Mirror.Of[Box[Int] | Box[String]]] // error -val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[String]]] // error -def baz = summon[deriving.Mirror.Of[Foo[Int] | Foo[String]]] // error +val foo = summon[Mirror.Of[Box[Int] | Box[Int]]] // error +val bar = summon[MirrorK1.Of[[X] =>> Box[Int] | Box[Int]]] // error +def baz = summon[deriving.Mirror.Of[Foo[String] | Foo[String]]] // error def qux = summon[deriving.Mirror.Of[Option[Int] | Option[String]]] // error From 588ee18c922d22fb9d85d23de516beaf4f91a052 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 25 Nov 2022 21:47:57 -0300 Subject: [PATCH 07/13] Cleanup --- compiler/src/dotty/tools/dotc/core/Types.scala | 18 ++++++++---------- .../tools/dotc/util/EqLinkedHashSet.scala | 12 ------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index c80e471c0da0..e5aaaa016361 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3491,28 +3491,26 @@ object Types { */ private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): MutableSet[Type] = { - var trees = List(this) + var unvisitedSubtrees = List(this) val uniqueTreeMembers = new EqLinkedHashSet[Type] - while (trees.nonEmpty) { - trees match { + while (unvisitedSubtrees.nonEmpty) { + unvisitedSubtrees match case head :: tail => - head match { + head match case OrType(l: OrType, r: OrType) => - trees = l :: r :: tail + unvisitedSubtrees = l :: r :: tail case OrType(l, r: OrType) => - trees = r :: tail + unvisitedSubtrees = r :: tail if !l.isNothingType then uniqueTreeMembers += l case OrType(l: OrType, r) => - trees = l :: tail + unvisitedSubtrees = l :: tail if !r.isNothingType then uniqueTreeMembers += r case OrType(l, r) => - trees = tail + unvisitedSubtrees = tail uniqueTreeMembers += l if !r.isNothingType then uniqueTreeMembers += r - } case _ => - } } uniqueTreeMembers diff --git a/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala index 73124d106ca6..1afd562a518b 100644 --- a/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala +++ b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala @@ -32,15 +32,3 @@ class EqLinkedHashSet[T]( override def iterator: Iterator[T] = linkingArray.iterator } - -object EqLinkedHashSet { - def apply[T](x: T): EqLinkedHashSet[T] = - val set = new EqLinkedHashSet[T] - set += x - set - - def apply[T](x: T, y: T): EqLinkedHashSet[T] = - val set = EqLinkedHashSet(x) - set += y - set -} From 896e92e21bb1ea983b5f56c976d1b60c0f2b3399 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Wed, 10 May 2023 10:09:16 -0300 Subject: [PATCH 08/13] updated EqLinkedHashSet.clear signature --- compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala index 1afd562a518b..3b5fe820da18 100644 --- a/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala +++ b/compiler/src/dotty/tools/dotc/util/EqLinkedHashSet.scala @@ -21,8 +21,8 @@ class EqLinkedHashSet[T]( map -= x if map.size != linkingArray.size then linkingArray -= x - override def clear(): Unit = - map.clear() + override def clear(resetToInitial: Boolean = true): Unit = + map.clear(resetToInitial) linkingArray.clear() override def lookup(x: T): T | Null = if map.contains(x) then x else null From 5974ca2e4212e8281c5a60b0e85372d08bb7a701 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Sat, 20 May 2023 14:35:44 -0300 Subject: [PATCH 09/13] deduplicatedAbsorbingNothingTypes checkpoint --- .../src/dotty/tools/dotc/core/Types.scala | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e5aaaa016361..2f076b1889d4 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3499,7 +3499,10 @@ object Types { case head :: tail => head match case OrType(l: OrType, r: OrType) => - unvisitedSubtrees = l :: r :: tail + if (l.isSoft && r.isSoft) + unvisitedSubtrees = l :: r :: tail + else if (!l.isSoft && r.isSoft) + unvisitedSubtrees = l :: r :: tail case OrType(l, r: OrType) => unvisitedSubtrees = r :: tail if !l.isNothingType then uniqueTreeMembers += l @@ -3520,20 +3523,21 @@ object Types { * Nothing types, if present. Weaker than LUB. */ def deduplicatedAbsorbingNothingTypes(using Context): Type = { - - val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes - val factorCount = orFactorCount(isSoft) - - uniqueTreeMembers.size match { - case 1 => - uniqueTreeMembers.iterator.next() - case uniqueMembersCount if uniqueMembersCount < factorCount => - val members = uniqueTreeMembers.iterator - val startingUnion = OrType(members.next(), members.next(), isSoft) - members.foldLeft(startingUnion)(OrType(_, _, isSoft)) - case _ => - this - } + if tp1 eq tp2 then tp1 + else + val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes + val factorCount = orFactorCount(isSoft) // TODO replace this by an actual softness check + + uniqueTreeMembers.size match { + case 1 => + uniqueTreeMembers.iterator.next() + case uniqueMembersCount if uniqueMembersCount < factorCount => + val members = uniqueTreeMembers.iterator + val startingUnion = OrType(members.next(), members.next(), isSoft) + members.foldLeft(startingUnion)(OrType(_, _, isSoft)) + case _ => + this + } } } From dbeb5b7374663e93b09d384078d1c406a1ae09f3 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 2 Jun 2023 21:59:25 -0300 Subject: [PATCH 10/13] boiled down test to issue code only --- tests/printing/i10693.check | 7 ------- tests/printing/i10693.scala | 8 -------- 2 files changed, 15 deletions(-) diff --git a/tests/printing/i10693.check b/tests/printing/i10693.check index 4c3842d75975..d7b9909085f5 100644 --- a/tests/printing/i10693.check +++ b/tests/printing/i10693.check @@ -11,13 +11,6 @@ package { val v4: String | Int = test[String | Int, Int | String](v2, v3) val v5: Int | String = test[Int | String, String | Int](v3, v2) val v6: String | Int = test[String | Int, Int | String](v4, v5) - val t0: List[Int] = Tuple1.apply[Int](1).toList - val t1: List[Int] = Tuple2.apply[Int, Int](1, 2).toList - val t2: List[Int] = Tuple3.apply[Int, Int, Int](1, 2, 3).toList - val t3: List[Int | String] = - Tuple3.apply[String, Int, Int]("A", 2, 3).toList - val t4: List[String | Int] = - Tuple3.apply[Int, String, String](1, "B", "C").toList } } diff --git a/tests/printing/i10693.scala b/tests/printing/i10693.scala index 0f852f70c3fd..122984484658 100644 --- a/tests/printing/i10693.scala +++ b/tests/printing/i10693.scala @@ -1,4 +1,3 @@ -// minimized code def test[A, B](a: A, b: B): A | B = a val v0 = test("string", 1) val v1 = test(1, "string") @@ -7,10 +6,3 @@ val v3 = test(v1, v0) val v4 = test(v2, v3) val v5 = test(v3, v2) val v6 = test(v4, v5) - -// issue comments section examples -val t0 = Tuple1(1).toList -val t1 = (1, 2).toList -val t2 = (1, 2, 3).toList -val t3 = ("A", 2, 3).toList -val t4 = (1, "B", "C").toList From 3335e61bbb71480e7fc7c9d9e25da9ddfe69fbc9 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Fri, 2 Jun 2023 22:01:24 -0300 Subject: [PATCH 11/13] deduplicating inferred val def types --- .../src/dotty/tools/dotc/core/TypeOps.scala | 21 +++++------ .../src/dotty/tools/dotc/core/Types.scala | 28 ++++++--------- .../src/dotty/tools/dotc/typer/Typer.scala | 35 +++++++++++++++++-- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeOps.scala b/compiler/src/dotty/tools/dotc/core/TypeOps.scala index d99fdc636f23..6809e4b9083c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeOps.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeOps.scala @@ -157,18 +157,15 @@ object TypeOps: tp.derivedAlias(simplify(tp.alias, theMap)) case AndType(l, r) if !ctx.mode.is(Mode.Type) => simplify(l, theMap) & simplify(r, theMap) - case tp @ OrType(l, r) => - if !ctx.mode.is(Mode.Type) && (tp.isSoft || l.isBottomType || r.isBottomType) then - // Normalize A | Null and Null | A to A even if the union is hard (i.e. - // explicitly declared), but not if -Yexplicit-nulls is set. The reason is - // that in this case the normal asSeenFrom machinery is not prepared to deal - // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take - // corrective steps, so no widening is wanted. - simplify(l, theMap) | simplify(r, theMap) - else if r.isNothingType || (l eq r) then l - else if l.isInstanceOf[OrType] || r.isInstanceOf[OrType] then tp.deduplicatedAbsorbingNothingTypes - else if l.isNothingType then r - else mapOver + case tp @ OrType(l, r) + if !ctx.mode.is(Mode.Type) + && (tp.isSoft || l.isBottomType || r.isBottomType) => + // Normalize A | Null and Null | A to A even if the union is hard (i.e. + // explicitly declared), but not if -Yexplicit-nulls is set. The reason is + // that in this case the normal asSeenFrom machinery is not prepared to deal + // with Nulls (which have no base classes). Under -Yexplicit-nulls, we take + // corrective steps, so no widening is wanted. + simplify(l, theMap) | simplify(r, theMap) case tp @ CapturingType(parent, refs) => if !ctx.mode.is(Mode.Type) && refs.subCaptures(parent.captureSet, frozen = true).isOK diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 2f076b1889d4..2bdf83d55d3e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3485,9 +3485,9 @@ object Types { case _ => false } - /** Returns the set of non-union (leaf) types composing this union tree with Nothing types - * absorbed by other types, if present. For example:
- * `(A | B | C | B | (A & (B | C)) | Nothing)` returns `{A, B, C, (A & (B | C))}`. + /** Returns the set of non-union (leaf) types composing this union tree. + * For example:
+ * `(A | B | A | B | (A & (B | C)))` returns `{A, B, (A & (B | C))}`. */ private def gatherTreeUniqueMembersAbsorbingNothingTypes(using Context): MutableSet[Type] = { @@ -3499,9 +3499,6 @@ object Types { case head :: tail => head match case OrType(l: OrType, r: OrType) => - if (l.isSoft && r.isSoft) - unvisitedSubtrees = l :: r :: tail - else if (!l.isSoft && r.isSoft) unvisitedSubtrees = l :: r :: tail case OrType(l, r: OrType) => unvisitedSubtrees = r :: tail @@ -3512,32 +3509,29 @@ object Types { case OrType(l, r) => unvisitedSubtrees = tail uniqueTreeMembers += l - if !r.isNothingType then uniqueTreeMembers += r + uniqueTreeMembers += r case _ => } uniqueTreeMembers } - /** Returns an equivalent union tree without repeated members, preserving order and absorbing - * Nothing types, if present. Weaker than LUB. + /** Returns an equivalent union tree without repeated members. Weaker than LUB. */ def deduplicatedAbsorbingNothingTypes(using Context): Type = { - if tp1 eq tp2 then tp1 - else + if tp1.isInstanceOf[OrType] || tp2.isInstanceOf[OrType] then val uniqueTreeMembers = this.gatherTreeUniqueMembersAbsorbingNothingTypes - val factorCount = orFactorCount(isSoft) // TODO replace this by an actual softness check uniqueTreeMembers.size match { case 1 => uniqueTreeMembers.iterator.next() - case uniqueMembersCount if uniqueMembersCount < factorCount => - val members = uniqueTreeMembers.iterator - val startingUnion = OrType(members.next(), members.next(), isSoft) - members.foldLeft(startingUnion)(OrType(_, _, isSoft)) case _ => - this + val members = uniqueTreeMembers.iterator + val startingUnion = OrType(members.next(), members.next(), soft = true) + members.foldLeft(startingUnion)(OrType(_, _, soft = true)) } + else if tp1 eq tp2 then tp1 + else this } } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7eb8519739c6..6eecf79534b0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2422,7 +2422,18 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer completeAnnotations(vdef, sym) if (sym.isOneOf(GivenOrImplicit)) checkImplicitConversionDefOK(sym) if sym.is(Module) then checkNoModuleClash(sym) - val tpt1 = checkSimpleKinded(typedType(tpt)) + val tpdType = typedType(tpt) + val tpt1: Tree = checkSimpleKinded(tpdType) match { + case inferred: InferredTypeTree => + println(i"inferred type = $inferred") + inferred.tpe match { + case or @ OrType(l, r) => + inferred.overwriteType(or.deduplicatedAbsorbingNothingTypes) + case _ => + } + inferred + case self => self + } val rhs1 = vdef.rhs match { case rhs @ Ident(nme.WILDCARD) => rhs withType tpt1.tpe case rhs => typedExpr(rhs, tpt1.tpe.widenExpr) @@ -3004,17 +3015,24 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedUnadapted(initTree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = { record("typedUnadapted") val xtree = expanded(initTree) + println("> typing unadapted") + println(i"initTree = $initTree") + println(i"pt = $pt") + xtree.removeAttachment(TypedAhead) match { case Some(ttree) => ttree case none => def typedNamed(tree: untpd.NameTree, pt: Type)(using Context): Tree = { val sym = retrieveSym(xtree) + println(s"$tree") tree match { case tree: untpd.Ident => typedIdent(tree, pt) case tree: untpd.Select => typedSelect(tree, pt) case tree: untpd.Bind => typedBind(tree, pt) case tree: untpd.ValDef => + println("> typing val def") + println(i"$tree") if (tree.isEmpty) tpd.EmptyValDef else typedValDef(tree, sym)(using ctx.localContext(tree, sym)) case tree: untpd.DefDef => @@ -3105,8 +3123,15 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer then makeContextualFunction(xtree, ifpt) else xtree match - case xtree: untpd.NameTree => typedNamed(xtree, pt) - case xtree => typedUnnamed(xtree) + case xtree: untpd.NameTree => + println(s"> typing named") + println(i"xtree = $xtree") + println(i"pt = $pt") + typedNamed(xtree, pt) + case xtree => + println(s"> typing unnamed") + println(i"xtree = $xtree") + typedUnnamed(xtree) simplify(result, pt, locked) catch case ex: TypeError => errorTree(xtree, ex, xtree.srcPos.focus) @@ -3169,6 +3194,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */ def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = + println("> typing tree") trace(i"typing $tree, pt = $pt", typr, show = true) { record(s"typed $getClass") record("typed total") @@ -3297,6 +3323,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer withoutMode(Mode.PatternOrTypeBits)(typed(tree, pt)) def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree = + println("> typing type") + println(i"pt = $pt") + println(s"tree = $tree") val tree1 = withMode(Mode.Type) { typed(tree, pt) } if mapPatternBounds && ctx.mode.is(Mode.Pattern) && !ctx.isAfterTyper then tree1 match From f540c3d2a57d0a9bd6fe7700b302115e20e00c03 Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Thu, 15 Jun 2023 21:40:10 -0300 Subject: [PATCH 12/13] updated i10693.check --- tests/printing/i10693.check | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/printing/i10693.check b/tests/printing/i10693.check index d7b9909085f5..3e1f5858d6a5 100644 --- a/tests/printing/i10693.check +++ b/tests/printing/i10693.check @@ -1,4 +1,4 @@ -[[syntax trees at end of typer]] // tests/printing/i10693.scala +[[syntax trees at end of typer]] // local/i10693.scala package { final lazy module val i10693$package: i10693$package = new i10693$package() final module class i10693$package() extends Object() { @@ -8,9 +8,13 @@ package { val v1: Int | String = test[Int, String](1, "string") val v2: String | Int = test[String | Int, Int | String](v0, v1) val v3: Int | String = test[Int | String, String | Int](v1, v0) - val v4: String | Int = test[String | Int, Int | String](v2, v3) - val v5: Int | String = test[Int | String, String | Int](v3, v2) - val v6: String | Int = test[String | Int, Int | String](v4, v5) - } + val v4: String | Int = + test[String | Int | (Int | String), Int | String | (String | Int)](v2, v3) + val v5: Int | String = + test[Int | String | (String | Int), String | Int | (Int | String)](v3, v2) + val v6: String | Int = + test[String | Int | (Int | String) | (Int | String | (String | Int)), + Int | String | (String | Int) | (String | Int | (Int | String))](v4, v5) + } } From 437150c1b0dd774dd60a6ce77259174927c9097c Mon Sep 17 00:00:00 2001 From: "Diego D. Mucciolo" Date: Thu, 15 Jun 2023 21:44:51 -0300 Subject: [PATCH 13/13] commenting prints --- .../src/dotty/tools/dotc/typer/Typer.scala | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 6eecf79534b0..58a2cb1a8b5e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2425,9 +2425,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val tpdType = typedType(tpt) val tpt1: Tree = checkSimpleKinded(tpdType) match { case inferred: InferredTypeTree => - println(i"inferred type = $inferred") +// println(i"inferred type = $inferred") inferred.tpe match { - case or @ OrType(l, r) => + case or: OrType => inferred.overwriteType(or.deduplicatedAbsorbingNothingTypes) case _ => } @@ -3015,9 +3015,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedUnadapted(initTree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = { record("typedUnadapted") val xtree = expanded(initTree) - println("> typing unadapted") - println(i"initTree = $initTree") - println(i"pt = $pt") +// println("> typing unadapted") +// println(i"initTree = $initTree") +// println(i"pt = $pt") xtree.removeAttachment(TypedAhead) match { case Some(ttree) => ttree @@ -3025,14 +3025,14 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer def typedNamed(tree: untpd.NameTree, pt: Type)(using Context): Tree = { val sym = retrieveSym(xtree) - println(s"$tree") +// println(i"sym = $sym") tree match { case tree: untpd.Ident => typedIdent(tree, pt) case tree: untpd.Select => typedSelect(tree, pt) case tree: untpd.Bind => typedBind(tree, pt) case tree: untpd.ValDef => - println("> typing val def") - println(i"$tree") +// println("> typing val def") +// println(i"$tree") if (tree.isEmpty) tpd.EmptyValDef else typedValDef(tree, sym)(using ctx.localContext(tree, sym)) case tree: untpd.DefDef => @@ -3124,13 +3124,13 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer makeContextualFunction(xtree, ifpt) else xtree match case xtree: untpd.NameTree => - println(s"> typing named") - println(i"xtree = $xtree") - println(i"pt = $pt") +// println(s"> typing named") +// println(i"xtree = $xtree") +// println(i"pt = $pt") typedNamed(xtree, pt) case xtree => - println(s"> typing unnamed") - println(i"xtree = $xtree") +// println(s"> typing unnamed") +// println(i"xtree = $xtree") typedUnnamed(xtree) simplify(result, pt, locked) @@ -3194,7 +3194,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer /** Typecheck and adapt tree, returning a typed tree. Parameters as for `typedUnadapted` */ def typed(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree = - println("> typing tree") +// println("> typing tree") trace(i"typing $tree, pt = $pt", typr, show = true) { record(s"typed $getClass") record("typed total") @@ -3323,9 +3323,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer withoutMode(Mode.PatternOrTypeBits)(typed(tree, pt)) def typedType(tree: untpd.Tree, pt: Type = WildcardType, mapPatternBounds: Boolean = false)(using Context): Tree = - println("> typing type") - println(i"pt = $pt") - println(s"tree = $tree") +// println("> typing type") +// println(i"tree = $tree") +// println(i"pt = $pt") val tree1 = withMode(Mode.Type) { typed(tree, pt) } if mapPatternBounds && ctx.mode.is(Mode.Pattern) && !ctx.isAfterTyper then tree1 match