Skip to content

Commit

Permalink
Handle TupleXXL in match analysis (#19212)
Browse files Browse the repository at this point in the history
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, after which SpaceEngine isIrrefutable needed
special handling to consider TupleXXL irrefutable.
  • Loading branch information
smarter committed Dec 7, 2023
2 parents d09bfda + 771b866 commit ee2b74d
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 15 deletions.
42 changes: 33 additions & 9 deletions compiler/src/dotty/tools/dotc/core/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,41 @@ class TypeUtils {
else None
recur(self.stripTypeVar, bound)

/** Is this a generic tuple that would fit into the range 1..22,
* but is not already an instance of one of Tuple1..22?
* In this case we need to cast it to make the TupleN/ members accessible.
* This works only for generic tuples of known size up to 22.
*/
def isSmallGenericTuple(using Context): Boolean =
/** Is this a generic tuple but not already an instance of one of Tuple1..22? */
def isGenericTuple(using Context): Boolean =
self.derivesFrom(defn.PairClass)
&& !defn.isTupleNType(self.widenDealias)
&& self.widenTermRefExpr.tupleElementTypesUpTo(Definitions.MaxTupleArity).match
case Some(elems) if elems.length <= Definitions.MaxTupleArity => true
case _ => false

/** Is this a generic tuple that would fit into the range 1..22?
* In this case we need to cast it to make the TupleN members accessible.
* This works only for generic tuples of known size up to 22.
*/
def isSmallGenericTuple(using Context): Boolean = genericTupleArityCompare < 0

/** Is this a generic tuple with an arity above 22? */
def isLargeGenericTuple(using Context): Boolean = genericTupleArityCompare > 0

/** If this is a generic tuple with element types, compare the arity and return:
* * -1, if the generic tuple is small (<= MaxTupleArity)
* * 1, if the generic tuple is large (> MaxTupleArity)
* * 0 if this isn't a generic tuple with element types
*/
def genericTupleArityCompare(using Context): Int =
if self.isGenericTuple then
self.widenTermRefExpr.tupleElementTypesUpTo(Definitions.MaxTupleArity).match
case Some(elems) => if elems.length <= Definitions.MaxTupleArity then -1 else 1
case _ => 0
else 0

/** Is this a large generic tuple and is `pat` TupleXXL?
* TupleXXL.unapplySeq extracts values of type TupleXXL
* but large scrutinee terms are typed as large generic tuples.
* This allows them to hold on to their precise element types,
* but it means type-wise, the terms don't conform to the
* extractor's parameter type, so this method identifies case.
*/
def isTupleXXLExtract(pat: Type)(using Context): Boolean =
pat.typeSymbol == defn.TupleXXLClass && self.isLargeGenericTuple

/** The `*:` equivalent of an instance of a Tuple class */
def toNestedPairs(using Context): Type =
Expand Down
10 changes: 5 additions & 5 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,9 @@ object SpaceEngine {
|| (unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) // scala2 compatibility
|| unapplySeqTypeElemTp(unappResult).exists // only for unapplySeq
|| isProductMatch(unappResult, argLen)
|| {
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition)
isEmptyTp <:< ConstantType(Constant(false))
}
|| extractorMemberType(unappResult, nme.isEmpty, NoSourcePosition) <:< ConstantType(Constant(false))
|| unappResult.derivesFrom(defn.NonEmptyTupleClass)
|| unapp.symbol == defn.TupleXXL_unapplySeq // Fixes TupleXXL.unapplySeq which returns Some but declares Option
}

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

Expand Down Expand Up @@ -836,7 +835,8 @@ object SpaceEngine {
def isCheckable(tp: Type): Boolean =
val tpw = tp.widen.dealias
val classSym = tpw.classSymbol
classSym.is(Sealed) ||
classSym.is(Sealed) && !tpw.isLargeGenericTuple || // exclude large generic tuples from exhaustivity
// requires an unknown number of changes to make work
tpw.isInstanceOf[OrType] ||
(tpw.isInstanceOf[AndType] && {
val and = tpw.asInstanceOf[AndType]
Expand Down
5 changes: 4 additions & 1 deletion compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,10 @@ trait Checking {
false
}

def check(pat: Tree, pt: Type): Boolean = (pt <:< pat.tpe) || fail(pat, pt, Reason.NonConforming)
def check(pat: Tree, pt: Type): Boolean =
pt.isTupleXXLExtract(pat.tpe) // See isTupleXXLExtract, fixes TupleXXL parameter type
|| pt <:< pat.tpe
|| fail(pat, pt, Reason.NonConforming)

def recur(pat: Tree, pt: Type): Boolean =
!sourceVersion.isAtLeast(`3.2`)
Expand Down
20 changes: 20 additions & 0 deletions tests/pos/i14588.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//> using options -Werror

class Test:
def t1: Unit =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) match
case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) =>
def t2: Unit =
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) match
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) =>
def t3: Unit =
(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
case (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23) =>

object Main:
def main(args: Array[String]): Unit = {
val t = new Test
t.t1
try { t.t2; ??? } catch case _: MatchError => ()
try { t.t3; ??? } catch case _: MatchError => ()
}
9 changes: 9 additions & 0 deletions tests/pos/i16186.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//> using options -Werror

class Test:
val x = 42
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)

tup23 match {
case (_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => "Tuple Pattern"
}
13 changes: 13 additions & 0 deletions tests/pos/i16657.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//> using options -Werror

class Test:
val (_, (
_, _, _, _, _, _, _, _, _, _, // 10
_, _, _, _, _, _, _, _, _, _, // 20
_, c22, _ // 23
)) = // nested pattern has 23 elems
(0, (
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
1, 2, 3, 4, 5, 6, 7, 8, 9, 20,
1, 2, 3
)) // ok, exhaustive, reachable, conforming and irrefutable
14 changes: 14 additions & 0 deletions tests/pos/i19084.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//> using options -Werror

class Test:
def t1(y: (
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
"Bob", Int, 33, Int,
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)
): Unit = y match
case b @ (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9,
"Bob", y1, 33, y2,
z0, z1, z2, z3, z4, z5, z6, z7, z8, z9)
=> // was: !!! spurious unreachable case warning
()
case _ => ()
17 changes: 17 additions & 0 deletions tests/warn/i19084.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@


class Test:
def t1(y: (
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
"Bob", Int, 33, Int,
Int, Int, Int, Int, Int, Int, Int, Int, Int, Int)
): Unit = y match
case b @ (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9,
"Bob", y1, 33, y2,
z0, z1, z2, z3, z4, z5, z6, z7, z8, z9)
=> ()
case b @ (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, // warn: unreachable
"Bob", y1, 33, y2,
z0, z1, z2, z3, z4, z5, z6, z7, z8, z9)
=> ()
case _ => ()

0 comments on commit ee2b74d

Please sign in to comment.