From 823f17b08e9edc87339d00fc2251fb8da18a326e Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Fri, 23 Jun 2023 12:19:43 +0100 Subject: [PATCH 1/7] Cleanup TreeChecker adapt assertion message The "infoStr" I felt caused more noise and confusion, so I remove it. I also pushed the type mismatch message done 1 line, so it displays better multi-line (the "Found" and "Required" are alined). --- .../tools/dotc/transform/TreeChecker.scala | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala index fcb20ea920e9..c1239b382498 100644 --- a/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala +++ b/compiler/src/dotty/tools/dotc/transform/TreeChecker.scala @@ -744,23 +744,16 @@ object TreeChecker { override def adapt(tree: Tree, pt: Type, locked: TypeVars)(using Context): Tree = { def isPrimaryConstructorReturn = ctx.owner.isPrimaryConstructor && pt.isRef(ctx.owner.owner) && tree.tpe.isRef(defn.UnitClass) - def infoStr(tp: Type) = tp match { - case tp: TypeRef => - val sym = tp.symbol - i"${sym.showLocated} with ${tp.designator}, flags = ${sym.flagsString}, underlying = ${tp.underlyingIterator.toList}%, %" - case _ => - "??" - } - if (ctx.mode.isExpr && - !tree.isEmpty && - !isPrimaryConstructorReturn && - !pt.isInstanceOf[FunOrPolyProto]) + if ctx.mode.isExpr + && !tree.isEmpty + && !isPrimaryConstructorReturn + && !pt.isInstanceOf[FunOrPolyProto] + then assert(tree.tpe <:< pt, { val mismatch = TypeMismatch(tree.tpe, pt, Some(tree)) - i"""|${mismatch.msg} - |found: ${infoStr(tree.tpe)} - |expected: ${infoStr(pt)} - |tree = $tree""".stripMargin + i"""|Type Mismatch: + |${mismatch.message} + |tree = $tree ${tree.className}""".stripMargin }) tree } From d3a877f6b543a9143bf24ee92e9f82f367555f5d Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Fri, 23 Jun 2023 12:25:18 +0100 Subject: [PATCH 2/7] Normalize in Type#atoms TypeComparer's compareAtoms assumes it has an answer to subtyping if one of the types has atoms and the other doesn't. But an unreduced match alias won't have atoms, so we shouldn't bail early. Co-Authored-By: Matt Bovel --- compiler/src/dotty/tools/dotc/core/Types.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 04dfbbb26ef7..965ef4ecfd9c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1373,7 +1373,7 @@ object Types { Atoms.Range(set, set) else Atoms.Unknown - dealias match + dealias.normalized match case tp: SingletonType => tp.underlying.atoms match case as @ Atoms.Range(lo, hi) => From 97dcf78f3ff04efa5c5bef8edf685de99129c5a3 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Fri, 23 Jun 2023 12:26:45 +0100 Subject: [PATCH 3/7] Widen scrutinees in TypeComparer's compareMatch If the skolem-widened scrutinees are the same and the cases correspond, then the match types are the same. Also, with inlining the type embeded in inlining's Typed expression will be without going through type-avoidance, so they won't be skolems. So we look to widen inline proxy term refs, in addition to skolems. Co-Authored-By: Matt Bovel --- .../dotty/tools/dotc/core/TypeComparer.scala | 5 ++++- tests/pos/16583.scala | 19 +++++++++++++++++++ tests/pos/16654.scala | 7 +++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/pos/16583.scala create mode 100644 tests/pos/16654.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index a0922c1f0574..4272ed15333f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1007,7 +1007,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: MatchType => def compareMatch = tp2 match { case tp2: MatchType => - isSameType(tp1.scrutinee, tp2.scrutinee) && + def widen(tp: Type) = tp match + case tp: TermRef if tp.symbol.is(InlineProxy) => tp.info + case tp => tp.widenSkolem + isSameType(widen(tp1.scrutinee), widen(tp2.scrutinee)) && tp1.cases.corresponds(tp2.cases)(isSubType) case _ => false } diff --git a/tests/pos/16583.scala b/tests/pos/16583.scala new file mode 100644 index 000000000000..06f9f01b1772 --- /dev/null +++ b/tests/pos/16583.scala @@ -0,0 +1,19 @@ +import scala.compiletime.constValueTuple + +val ll0: Tuple3["one", "two", "three"] = constValueTuple[("one", "two", "three")] +val ll1 = constValueTuple[("one", "two", "three")].toList +val ll3: List["one" | ("two" | ("three" | Nothing))] = constValueTuple[("one", "two", "three")].toList +val ll4: List["one" | ("two" | "three")] = constValueTuple[("one", "two", "three")].toList + +inline def labels[Labels <: Tuple](using ev: Tuple.Union[Labels] <:< String): List[String] = + val tmp = constValueTuple[Labels].toList + ev.substituteCo(tmp) + +def test = labels[("one", "two", "three")] + +def toList(x: Tuple): List[Tuple.Union[x.type]] = ??? +def test2[Labels <: Tuple] = toList((???): Labels) + +def i16654 = + def t1: Tuple = EmptyTuple + val t2 = t1.toList diff --git a/tests/pos/16654.scala b/tests/pos/16654.scala new file mode 100644 index 000000000000..9234c309de88 --- /dev/null +++ b/tests/pos/16654.scala @@ -0,0 +1,7 @@ +def toCsvFlat[A <: Product](a: A)(using m: scala.deriving.Mirror.ProductOf[A]) = { + def flatTuple(any: Any): Tuple = any match + case p: Product => p.productIterator.map(flatTuple).foldLeft(EmptyTuple: Tuple)(_ ++ _) + case a => Tuple1(a) + + val tuple = flatTuple(Tuple.fromProductTyped(a)).toList +} From fcc753b081c9f3ac11021a5a8838728e2c04ed4d Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Fri, 23 Jun 2023 16:27:07 +0100 Subject: [PATCH 4/7] Test term ref scrutinees aren't generally widened Co-Authored-By: Matt Bovel --- tests/neg/mt-scrutinee-widen.scala | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/neg/mt-scrutinee-widen.scala diff --git a/tests/neg/mt-scrutinee-widen.scala b/tests/neg/mt-scrutinee-widen.scala new file mode 100644 index 000000000000..b9cb5c8769f4 --- /dev/null +++ b/tests/neg/mt-scrutinee-widen.scala @@ -0,0 +1,24 @@ +// We widen scrutinee's that are inline proxies +// But make sure that term refs in scrutinees are not widened in general + +val x: Int = 42 +val y: Int = 43 +val z: Int = 44 + +type IsX[T] = + T match + case x.type => true + case _ => false +def test = summon[IsX[y.type] =:= IsX[z.type]] // error + +def test2 = summon[ + ( + y.type match + case x.type => true + case _ => false + ) =:= ( + z.type match + case x.type => true + case _ => false + ) +] // error From 7825d77c01adb4a97b2f06242827a79d8e2412e5 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 27 Jun 2023 14:58:04 +0100 Subject: [PATCH 5/7] In compareMatch, check scrutinees are subtypes From the Match Type paper, typing rule S-Match5 requires that the LHS scrutinee "Ss" is a subtype of the RHS scrutinee "Ts": Ss <: Ts. It's not required that they are the same type. This covers widening a skolem or inline proxy tp1 scrutinee, without erroneously widening the RHS tp2 scrutinee. --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 5 +---- tests/neg/mt-scrutinee-widen2.scala | 13 +++++++++++++ tests/pos/mt-scrutinee-widen3.scala | 12 ++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 tests/neg/mt-scrutinee-widen2.scala create mode 100644 tests/pos/mt-scrutinee-widen3.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 4272ed15333f..e08192ae973c 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1007,10 +1007,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: MatchType => def compareMatch = tp2 match { case tp2: MatchType => - def widen(tp: Type) = tp match - case tp: TermRef if tp.symbol.is(InlineProxy) => tp.info - case tp => tp.widenSkolem - isSameType(widen(tp1.scrutinee), widen(tp2.scrutinee)) && + isSubType(tp1.scrutinee, tp2.scrutinee) && tp1.cases.corresponds(tp2.cases)(isSubType) case _ => false } diff --git a/tests/neg/mt-scrutinee-widen2.scala b/tests/neg/mt-scrutinee-widen2.scala new file mode 100644 index 000000000000..8e89f6ab7122 --- /dev/null +++ b/tests/neg/mt-scrutinee-widen2.scala @@ -0,0 +1,13 @@ +// A test case showing how we shouldn't widen +// both IsX scrutinees and make "def test" typecheck +import scala.util.Random +val x = 42 + +type IsX[T] = + T match + case x.type => true + case _ => false + +def bothXOrNot(a: Int, b: Int)(using IsX[a.type] =:= IsX[b.type]) = ??? + +def test = bothXOrNot(Random.nextInt(), Random.nextInt()) // error diff --git a/tests/pos/mt-scrutinee-widen3.scala b/tests/pos/mt-scrutinee-widen3.scala new file mode 100644 index 000000000000..6e06cb4e1d4c --- /dev/null +++ b/tests/pos/mt-scrutinee-widen3.scala @@ -0,0 +1,12 @@ +// Like widen2, but using a.type only, meaning it should typecheck +import scala.util.Random +val x = 42 + +type IsX[T] = + T match + case x.type => true + case _ => false + +def bothXOrNot(a: Int, b: Int)(using IsX[a.type] =:= IsX[a.type]) = ??? + +def test = bothXOrNot(Random.nextInt(), Random.nextInt()) From 27ae379d568c6b6bf9c9213a80fc7fe48f4482b0 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 28 Jun 2023 11:21:45 +0100 Subject: [PATCH 6/7] Par back to only widening skolems and inline proxies --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 5 ++++- tests/neg/mt-subtyping-transitivity.scala | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/neg/mt-subtyping-transitivity.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index e08192ae973c..ff4a00a4d68f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1007,7 +1007,10 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: MatchType => def compareMatch = tp2 match { case tp2: MatchType => - isSubType(tp1.scrutinee, tp2.scrutinee) && + val scrutinee1 = tp1.scrutinee match + case tp: TermRef if tp.symbol.is(InlineProxy) => tp.info + case tp => tp.widenSkolem + isSameType(scrutinee1, tp2.scrutinee) && tp1.cases.corresponds(tp2.cases)(isSubType) case _ => false } diff --git a/tests/neg/mt-subtyping-transitivity.scala b/tests/neg/mt-subtyping-transitivity.scala new file mode 100644 index 000000000000..d654f2a45258 --- /dev/null +++ b/tests/neg/mt-subtyping-transitivity.scala @@ -0,0 +1,14 @@ +final class A +final class B + +type MT[X] = X match + case A => String + case B => Int + +def test: MT[A | B] = ??? : MT[A] // error +// testing that +// MT[A] !<: MT[A | B] +// otherwise +// String <: MT[A] <: MT[A | B] +// but +// String !<: MT[A | B] From 91c9afc459588966d4cf3fdd1e70f7bfbc840f39 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 28 Jun 2023 14:25:37 +0100 Subject: [PATCH 7/7] Document widenScrutinee & check original scrutinee first --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index ff4a00a4d68f..3aa0528cf982 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1007,10 +1007,18 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling case tp1: MatchType => def compareMatch = tp2 match { case tp2: MatchType => - val scrutinee1 = tp1.scrutinee match - case tp: TermRef if tp.symbol.is(InlineProxy) => tp.info - case tp => tp.widenSkolem - isSameType(scrutinee1, tp2.scrutinee) && + // we allow a small number of scrutinee types to be widened: + // * skolems, which may appear from type avoidance, but are widened in the inferred result type + // * inline proxies, which is inlining's solution to the same problem + def widenScrutinee(scrutinee1: Type) = scrutinee1 match + case tp: TermRef if tp.symbol.is(InlineProxy) => tp.info + case tp => tp.widenSkolem + def checkScrutinee(scrutinee1: Type): Boolean = + isSameType(scrutinee1, tp2.scrutinee) || { + val widenScrutinee1 = widenScrutinee(scrutinee1) + (widenScrutinee1 ne scrutinee1) && checkScrutinee(widenScrutinee1) + } + checkScrutinee(tp1.scrutinee) && tp1.cases.corresponds(tp2.cases)(isSubType) case _ => false }