Skip to content

Commit 4105afb

Browse files
Merge pull request #14434 from dotty-staging/fix-11008
Fix #11008: Support generic tuples as a valid unapply result
2 parents 153b91a + 69b884d commit 4105afb

File tree

6 files changed

+47
-7
lines changed

6 files changed

+47
-7
lines changed

compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import collection.mutable
88
import Symbols._, Contexts._, Types._, StdNames._, NameOps._
99
import ast.Trees._
1010
import util.Spans._
11-
import typer.Applications.{isProductMatch, isGetMatch, isProductSeqMatch, productSelectors, productArity, unapplySeqTypeElemTp}
11+
import typer.Applications.*
1212
import SymUtils._
1313
import Flags._, Constants._
1414
import Decorators._
@@ -329,15 +329,16 @@ object PatternMatcher {
329329
def isSyntheticScala2Unapply(sym: Symbol) =
330330
sym.isAllOf(SyntheticCase) && sym.owner.is(Scala2x)
331331

332+
def tupleApp(i: Int, receiver: Tree) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method
333+
ref(defn.RuntimeTuplesModule)
334+
.select(defn.RuntimeTuples_apply)
335+
.appliedTo(receiver, Literal(Constant(i)))
336+
.cast(args(i).tpe.widen)
337+
332338
if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length)
333339
def tupleSel(sym: Symbol) = ref(scrutinee).select(sym)
334-
def tupleApp(i: Int) = // manually inlining the call to NonEmptyTuple#apply, because it's an inline method
335-
ref(defn.RuntimeTuplesModule)
336-
.select(defn.RuntimeTuples_apply)
337-
.appliedTo(ref(scrutinee), Literal(Constant(i)))
338-
.cast(args(i).tpe.widen)
339340
val isGenericTuple = defn.isTupleClass(caseClass) && !defn.isTupleNType(tree.tpe)
340-
val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp) else caseAccessors.map(tupleSel)
341+
val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel)
341342
matchArgsPlan(components, args, onSuccess)
342343
else if (unapp.tpe <:< (defn.BooleanType))
343344
TestPlan(GuardTest, unapp, unapp.span, onSuccess)
@@ -356,6 +357,9 @@ object PatternMatcher {
356357
else if (isUnapplySeq && unapplySeqTypeElemTp(unapp.tpe.widen.finalResultType).exists) {
357358
unapplySeqPlan(unappResult, args)
358359
}
360+
else if unappResult.info <:< defn.NonEmptyTupleTypeRef then
361+
val components = (0 until foldApplyTupleType(unappResult.denot.info).length).toList.map(tupleApp(_, ref(unappResult)))
362+
matchArgsPlan(components, args, onSuccess)
359363
else {
360364
assert(isGetMatch(unapp.tpe))
361365
val argsPlan = {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ object Applications {
173173
val elemTp = unapplySeqTypeElemTp(tp)
174174
if (elemTp.exists) args.map(Function.const(elemTp))
175175
else if (isProductSeqMatch(tp, args.length, pos)) productSeqSelectors(tp, args.length, pos)
176+
else if tp.derivesFrom(defn.NonEmptyTupleClass) then foldApplyTupleType(tp)
176177
else fallback
177178
}
178179

@@ -193,10 +194,22 @@ object Applications {
193194
productSelectorTypes(unapplyResult, pos)
194195
// this will cause a "wrong number of arguments in pattern" error later on,
195196
// which is better than the message in `fail`.
197+
else if unapplyResult.derivesFrom(defn.NonEmptyTupleClass) then
198+
foldApplyTupleType(unapplyResult)
196199
else fail
197200
}
198201
}
199202

203+
def foldApplyTupleType(tp: Type)(using Context): List[Type] =
204+
object tupleFold extends TypeAccumulator[List[Type]]:
205+
override def apply(accum: List[Type], t: Type): List[Type] =
206+
t match
207+
case AppliedType(tycon, x :: x2 :: Nil) if tycon.typeSymbol == defn.PairClass =>
208+
apply(x :: accum, x2)
209+
case x => foldOver(accum, x)
210+
end tupleFold
211+
tupleFold(Nil, tp).reverse
212+
200213
def wrapDefs(defs: mutable.ListBuffer[Tree], tree: Tree)(using Context): Tree =
201214
if (defs != null && defs.nonEmpty) tpd.Block(defs.toList, tree) else tree
202215

tests/run/i11008.check

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
hello
2+
hello and 10

tests/run/i11008.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
object A:
2+
def unapply(s: String): String *: EmptyTuple = Tuple1(s)
3+
4+
object B:
5+
def unapply(s:String): String *: Int *: EmptyTuple = Tuple2(s, 10)
6+
7+
@main def Test =
8+
"hello" match
9+
case A(x) =>
10+
println(x)
11+
"hello" match
12+
case B(x, y) =>
13+
println(s"$x and $y")

tests/run/i11008b.check

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Many 3 characters List(f, o, o)

tests/run/i11008b.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
object Foo:
2+
def unapplySeq(x: String): Int *: Seq[String] *: EmptyTuple = (x.length, x.toList.map(_.toString))
3+
4+
@main def Test =
5+
"foo" match
6+
case Foo(1, c) => println("One character " + c)
7+
case Foo(x, xs*) => println(s"Many $x characters $xs")

0 commit comments

Comments
 (0)