Skip to content

Commit e56b75e

Browse files
committed
Handle TupleXXL in match analysis
There's a number of problems with the match analysis of TupleXXL. Of course, they manifest as (in)exhaustivity and (un)reachability warnings. Reachability suffered by the problem that a large generic tuple scrutinee type wasn't considered extractable by the TupleXXL extractor that Typer associates the extractor pattern with. That was solved by special handling in SpaceEngine's isSubType. Exhaustivity suffered by a variety of problems, again stemming from the disconnect between the TupleXXL pattern type and the large generic tuple scrutinee (or component) type. That was solved by special handling in exhaustivityCheckable to ignore large generic tuple scrutinees. That then highlighted an irrefutable failure (checkIrrefutable), which also needed to be taught that extra large generic tuples do conform to TupleXXL extractors type, afterwhich SpaceEngine isIrrefutable needed special handling to consider TuplXXL irrefutable.
1 parent d96e9e4 commit e56b75e

File tree

7 files changed

+97
-15
lines changed

7 files changed

+97
-15
lines changed

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,41 @@ class TypeUtils {
7272
else None
7373
recur(self.stripTypeVar, bound)
7474

75-
/** Is this a generic tuple that would fit into the range 1..22,
76-
* but is not already an instance of one of Tuple1..22?
77-
* In this case we need to cast it to make the TupleN/ members accessible.
78-
* This works only for generic tuples of known size up to 22.
79-
*/
80-
def isSmallGenericTuple(using Context): Boolean =
75+
/** Is this a generic tuple but not already an instance of one of Tuple1..22? */
76+
def isGenericTuple(using Context): Boolean =
8177
self.derivesFrom(defn.PairClass)
8278
&& !defn.isTupleNType(self.widenDealias)
83-
&& self.widenTermRefExpr.tupleElementTypesUpTo(Definitions.MaxTupleArity).match
84-
case Some(elems) if elems.length <= Definitions.MaxTupleArity => true
85-
case _ => false
79+
80+
/** Is this a generic tuple that would fit into the range 1..22?
81+
* In this case we need to cast it to make the TupleN members accessible.
82+
* This works only for generic tuples of known size up to 22.
83+
*/
84+
def isSmallGenericTuple(using Context): Boolean = genericTupleArityCompare < 0
85+
86+
/** Is this a generic tuple with an arity above 22? */
87+
def isLargeGenericTuple(using Context): Boolean = genericTupleArityCompare > 0
88+
89+
/** If this is a generic tuple with element types, compare the arity and return:
90+
* * -1, if the generic tuple is small (<= MaxTupleArity)
91+
* * 1, if the generic tuple is large (> MaxTupleArity)
92+
* * 0 if this isn't a generic tuple with element types
93+
*/
94+
def genericTupleArityCompare(using Context): Int =
95+
if self.isGenericTuple then
96+
self.widenTermRefExpr.tupleElementTypesUpTo(Definitions.MaxTupleArity).match
97+
case Some(elems) => if elems.length <= Definitions.MaxTupleArity then -1 else 1
98+
case _ => 0
99+
else 0
100+
101+
/** Is this a large generic tuple and is `pat` TupleXXL?
102+
* TupleXXL.unapplySeq extracts values of type TupleXXL
103+
* but large scrutinee terms are typed as large generic tuples.
104+
* This allows them to hold on to their precise element types,
105+
* but it means type-wise, the terms don't conform to the
106+
* extractor's parameter type, so this method identifies case.
107+
*/
108+
def isTupleXXLExtract(pat: Type)(using Context): Boolean =
109+
pat.typeSymbol == defn.TupleXXLClass && self.isLargeGenericTuple
86110

87111
/** The `*:` equivalent of an instance of a Tuple class */
88112
def toNestedPairs(using Context): Type =

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,9 @@ object SpaceEngine {
287287
|| (unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) // scala2 compatibility
288288
|| unapplySeqTypeElemTp(unappResult).exists // only for unapplySeq
289289
|| isProductMatch(unappResult, argLen)
290-
|| {
291-
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition)
292-
isEmptyTp <:< ConstantType(Constant(false))
293-
}
290+
|| extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition) <:< ConstantType(Constant(false))
294291
|| unappResult.derivesFrom(defn.NonEmptyTupleClass)
292+
|| unapp.symbol == defn.TupleXXL_unapplySeq // Fixes TupleXXL.unapplySeq which returns Some but declares Option
295293
}
296294

297295
/** Is the unapply or unapplySeq irrefutable?
@@ -505,6 +503,7 @@ object SpaceEngine {
505503
def isSubType(tp1: Type, tp2: Type)(using Context): Boolean = trace(i"$tp1 <:< $tp2", debug, show = true) {
506504
if tp1 == ConstantType(Constant(null)) && !ctx.mode.is(Mode.SafeNulls)
507505
then tp2 == ConstantType(Constant(null))
506+
else if tp1.isTupleXXLExtract(tp2) then true // See isTupleXXLExtract, fixes TupleXXL parameter type
508507
else tp1 <:< tp2
509508
}
510509

@@ -836,7 +835,7 @@ object SpaceEngine {
836835
def isCheckable(tp: Type): Boolean =
837836
val tpw = tp.widen.dealias
838837
val classSym = tpw.classSymbol
839-
classSym.is(Sealed) ||
838+
classSym.is(Sealed) && !tpw.isLargeGenericTuple || // exclude large generic tuples from exhaustivity
840839
tpw.isInstanceOf[OrType] ||
841840
(tpw.isInstanceOf[AndType] && {
842841
val and = tpw.asInstanceOf[AndType]

compiler/src/dotty/tools/dotc/typer/Checking.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,10 @@ trait Checking {
936936
false
937937
}
938938

939-
def check(pat: Tree, pt: Type): Boolean = (pt <:< pat.tpe) || fail(pat, pt, Reason.NonConforming)
939+
def check(pat: Tree, pt: Type): Boolean =
940+
pt.isTupleXXLExtract(pat.tpe) // See isTupleXXLExtract, fixes TupleXXL parameter type
941+
|| pt <:< pat.tpe
942+
|| fail(pat, pt, Reason.NonConforming)
940943

941944
def recur(pat: Tree, pt: Type): Boolean =
942945
!sourceVersion.isAtLeast(`3.2`)

tests/pos/i14588.scala

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//> using options -Werror
2+
3+
class Test:
4+
def t1: Unit =
5+
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) match
6+
case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) =>
7+
def t2: Unit =
8+
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) match
9+
case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23, x24) =>
10+
def t3: Unit =
11+
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24) match
12+
case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) =>
13+
14+
object Main:
15+
def main(args: Array[String]): Unit = {
16+
val t = new Test
17+
t.t1
18+
try { t.t2; ??? } catch case _: MatchError => ()
19+
try { t.t3; ??? } catch case _: MatchError => ()
20+
}

tests/pos/i16186.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//> using options -Werror
2+
3+
class Test:
4+
val x = 42
5+
val tup23 = (x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x)
6+
7+
tup23 match {
8+
case (_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => "Tuple Pattern"
9+
}

tests/pos/i16657.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//> using options -Werror
2+
3+
class Test:
4+
val (_, (
5+
_, _, _, _, _, _, _, _, _, _, // 10
6+
_, _, _, _, _, _, _, _, _, _, // 20
7+
_, c22, _ // 23
8+
)) = // nested pattern has 23 elems
9+
(0, (
10+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11+
1, 2, 3, 4, 5, 6, 7, 8, 9, 20,
12+
1, 2, 3
13+
)) // ok, exhaustive, reachable, conforming and irrefutable

tests/pos/i19084.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//> using options -Werror
2+
3+
class Test:
4+
def t1(y: (
5+
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
6+
"Bob", Int, 33, Int,
7+
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)
8+
): Unit = y match
9+
case b @ (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9,
10+
"Bob", y1, 33, y2,
11+
z0, z1, z2, z3, z4, z5, z6, z7, z8, z9)
12+
=> // was: !!! spurious unreachable case warning
13+
()
14+
case _ => ()

0 commit comments

Comments
 (0)