Skip to content

Commit 376f5b0

Browse files
Merge pull request #6850 from dotty-staging/fix-6490
Fix #6849: support irrefutable sequence match
2 parents 4bd9400 + 435f02f commit 376f5b0

18 files changed

+219
-32
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,8 @@ object desugar {
715715
val methName = if (hasRepeatedParam) nme.unapplySeq else nme.unapply
716716
val unapplyParam = makeSyntheticParameter(tpt = classTypeRef)
717717
val unapplyRHS = if (arity == 0) Literal(Constant(true)) else Ident(unapplyParam.name)
718-
DefDef(methName, derivedTparams, (unapplyParam :: Nil) :: Nil, TypeTree(), unapplyRHS)
718+
val unapplyResTp = if (arity == 0) Literal(Constant(true)) else TypeTree()
719+
DefDef(methName, derivedTparams, (unapplyParam :: Nil) :: Nil, unapplyResTp, unapplyRHS)
719720
.withMods(synthetic)
720721
}
721722
companionDefs(companionParent, applyMeths ::: unapplyMeth :: companionMembers)

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

+5-2
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}
11+
import typer.Applications.{isProductMatch, isGetMatch, isProductSeqMatch, productSelectors, productArity, unapplySeqTypeElemTp}
1212
import SymUtils._
1313
import Flags._, Constants._
1414
import Decorators._
@@ -329,10 +329,13 @@ object PatternMatcher {
329329
.map(ref(unappResult).select(_))
330330
matchArgsPlan(selectors, args, onSuccess)
331331
}
332-
else if (isProductSeqMatch(unapp.tpe.widen, args.length, unapp.sourcePos) && isUnapplySeq) {
332+
else if (isUnapplySeq && isProductSeqMatch(unapp.tpe.widen, args.length, unapp.sourcePos)) {
333333
val arity = productArity(unapp.tpe.widen, unapp.sourcePos)
334334
unapplyProductSeqPlan(unappResult, args, arity)
335335
}
336+
else if (isUnapplySeq && unapplySeqTypeElemTp(unapp.tpe.widen.finalResultType).exists) {
337+
unapplySeqPlan(unappResult, args)
338+
}
336339
else {
337340
assert(isGetMatch(unapp.tpe))
338341
val argsPlan = {

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

+41-19
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,30 @@ object SpaceEngine {
275275
/** Is the unapply irrefutable?
276276
* @param unapp The unapply function reference
277277
*/
278-
def isIrrefutableUnapply(unapp: tpd.Tree)(implicit ctx: Context): Boolean = {
278+
def isIrrefutableUnapply(unapp: tpd.Tree, patSize: Int)(implicit ctx: Context): Boolean = {
279279
val unappResult = unapp.tpe.widen.finalResultType
280280
unappResult.isRef(defn.SomeClass) ||
281-
unappResult =:= ConstantType(Constant(true)) ||
282-
(unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) ||
283-
productArity(unappResult) > 0
281+
unappResult <:< ConstantType(Constant(true)) ||
282+
(unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) || // scala2 compatibility
283+
(patSize != -1 && productArity(unappResult) == patSize) || {
284+
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, unapp.sourcePos)
285+
isEmptyTp <:< ConstantType(Constant(false))
286+
}
287+
}
288+
289+
/** Is the unapplySeq irrefutable?
290+
* @param unapp The unapplySeq function reference
291+
*/
292+
def isIrrefutableUnapplySeq(unapp: tpd.Tree, patSize: Int)(implicit ctx: Context): Boolean = {
293+
val unappResult = unapp.tpe.widen.finalResultType
294+
unappResult.isRef(defn.SomeClass) ||
295+
(unapp.symbol.is(Synthetic) && unapp.symbol.owner.linkedClass.is(Case)) || // scala2 compatibility
296+
unapplySeqTypeElemTp(unappResult).exists ||
297+
isProductSeqMatch(unappResult, patSize) ||
298+
{
299+
val isEmptyTp = extractorMemberType(unappResult, nme.isEmpty, unapp.sourcePos)
300+
isEmptyTp <:< ConstantType(Constant(false))
301+
}
284302
}
285303
}
286304

@@ -348,16 +366,15 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
348366
else {
349367
val (arity, elemTp, resultTp) = unapplySeqInfo(fun.tpe.widen.finalResultType, fun.sourcePos)
350368
if (elemTp.exists)
351-
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, projectSeq(pats) :: Nil, isIrrefutableUnapply(fun))
369+
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, projectSeq(pats) :: Nil, isIrrefutableUnapplySeq(fun, pats.size))
352370
else
353-
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)),isIrrefutableUnapply(fun))
371+
Prod(erase(pat.tpe.stripAnnots), fun.tpe, fun.symbol, pats.take(arity - 1).map(project) :+ projectSeq(pats.drop(arity - 1)), isIrrefutableUnapplySeq(fun, pats.size))
354372
}
355373
else
356-
Prod(erase(pat.tpe.stripAnnots), erase(fun.tpe), fun.symbol, pats.map(project), isIrrefutableUnapply(fun))
357-
case Typed(expr @ Ident(nme.WILDCARD), tpt) =>
374+
Prod(erase(pat.tpe.stripAnnots), erase(fun.tpe), fun.symbol, pats.map(project), isIrrefutableUnapply(fun, pats.length))
375+
case Typed(pat @ UnApply(_, _, _), _) => project(pat)
376+
case Typed(expr, _) =>
358377
Typ(erase(expr.tpe.stripAnnots), true)
359-
case Typed(pat, _) =>
360-
project(pat)
361378
case This(_) =>
362379
Typ(pat.tpe.stripAnnots, false)
363380
case EmptyTree => // default rethrow clause of try/catch, check tests/patmat/try2.scala
@@ -678,19 +695,21 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
678695
companion.findMember(nme.unapplySeq, NoPrefix, required = EmptyFlags, excluded = Synthetic).exists
679696
}
680697

681-
def doShow(s: Space, mergeList: Boolean = false): String = s match {
698+
def doShow(s: Space, flattenList: Boolean = false): String = s match {
682699
case Empty => ""
683700
case Typ(c: ConstantType, _) => "" + c.value.value
684-
case Typ(tp: TermRef, _) => tp.symbol.showName
701+
case Typ(tp: TermRef, _) =>
702+
if (flattenList && tp <:< scalaNilType) ""
703+
else tp.symbol.showName
685704
case Typ(tp, decomposed) =>
686705
val sym = tp.widen.classSymbol
687706

688707
if (ctx.definitions.isTupleType(tp))
689708
params(tp).map(_ => "_").mkString("(", ", ", ")")
690709
else if (scalaListType.isRef(sym))
691-
if (mergeList) "_: _*" else "_: List"
710+
if (flattenList) "_: _*" else "_: List"
692711
else if (scalaConsType.isRef(sym))
693-
if (mergeList) "_, _: _*" else "List(_, _: _*)"
712+
if (flattenList) "_, _: _*" else "List(_, _: _*)"
694713
else if (tp.classSymbol.is(Sealed) && tp.classSymbol.hasAnonymousChild)
695714
"_: " + showType(tp) + " (anonymous)"
696715
else if (tp.classSymbol.is(CaseClass) && !hasCustomUnapply(tp.classSymbol))
@@ -702,15 +721,18 @@ class SpaceEngine(implicit ctx: Context) extends SpaceLogic {
702721
if (ctx.definitions.isTupleType(tp))
703722
"(" + params.map(doShow(_)).mkString(", ") + ")"
704723
else if (tp.isRef(scalaConsType.symbol))
705-
if (mergeList) params.map(doShow(_, mergeList)).mkString(", ")
706-
else params.map(doShow(_, true)).filter(_ != "Nil").mkString("List(", ", ", ")")
707-
else
708-
showType(sym.owner.typeRef) + params.map(doShow(_)).mkString("(", ", ", ")")
724+
if (flattenList) params.map(doShow(_, flattenList)).mkString(", ")
725+
else params.map(doShow(_, flattenList = true)).filter(!_.isEmpty).mkString("List(", ", ", ")")
726+
else {
727+
val isUnapplySeq = sym.name.eq(nme.unapplySeq)
728+
val paramsStr = params.map(doShow(_, flattenList = isUnapplySeq)).mkString("(", ", ", ")")
729+
showType(sym.owner.typeRef) + paramsStr
730+
}
709731
case Or(_) =>
710732
throw new Exception("incorrect flatten result " + s)
711733
}
712734

713-
flatten(s).map(doShow(_, false)).distinct.mkString(", ")
735+
flatten(s).map(doShow(_, flattenList = false)).distinct.mkString(", ")
714736
}
715737

716738
private def exhaustivityCheckable(sel: Tree): Boolean = {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ object Applications {
7171
* parameterless `isEmpty` member of result type `Boolean`.
7272
*/
7373
def isGetMatch(tp: Type, errorPos: SourcePosition = NoSourcePosition)(implicit ctx: Context): Boolean =
74-
extractorMemberType(tp, nme.isEmpty, errorPos).isRef(defn.BooleanClass) &&
74+
extractorMemberType(tp, nme.isEmpty, errorPos).widenSingleton.isRef(defn.BooleanClass) &&
7575
extractorMemberType(tp, nme.get, errorPos).exists
7676

7777
/** If `getType` is of the form:

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ trait Checking {
643643
recur(pat1, pt)
644644
case UnApply(fn, _, pats) =>
645645
check(pat, pt) &&
646-
(isIrrefutableUnapply(fn) || fail(pat, pt)) && {
646+
(isIrrefutableUnapply(fn, pats.length) || fail(pat, pt)) && {
647647
val argPts = unapplyArgs(fn.tpe.widen.finalResultType, fn, pats, pat.sourcePos)
648648
pats.corresponds(argPts)(recur)
649649
}

compiler/test/dotc/run-test-pickling.blacklist

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ macros-in-same-project1
2424
i5257.scala
2525
i7868.scala
2626
enum-java
27+
zero-arity-case-class.scala
28+
tuple-ops.scala

docs/docs/reference/changed-features/pattern-matching.md

+13
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ and `S` conforms to one of the following matches:
5050
The former form of `unapply` has higher precedence, and _single match_ has higher
5151
precedence over _name-based match_.
5252

53+
A usage of a fixed-arity extractor is irrefutable if one of the following condition holds:
54+
55+
- `U = true`
56+
- the extractor is used as a product match
57+
- `U = Some[T]` (for Scala2 compatibility)
58+
- `U <: R` and `U <: { def isEmpty: false }`
59+
5360
### Variadic Extractors
5461

5562
Variadic extractors expose the following signature:
@@ -77,6 +84,12 @@ and `S` conforms to one of the two matches above.
7784
The former form of `unapplySeq` has higher priority, and _sequence match_ has higher
7885
precedence over _product-sequence match_.
7986

87+
A usage of a variadic extractor is irrefutable if one of the following condition holds:
88+
89+
- the extractor is used directly as a sequence match or product-sequence match
90+
- `U = Some[T]` (for Scala2 compatibility)
91+
- `U <: R` and `U <: { def isEmpty: false }`
92+
8093
## Boolean Match
8194

8295
- `U =:= Boolean`

tests/patmat/i6490.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8: Pattern Match Exhaustivity: Foo()

tests/patmat/i6490.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
sealed class Foo(val value: Int)
2+
3+
object Foo {
4+
def unapplySeq(foo: Foo): Seq[Int] = List(foo.value)
5+
}
6+
7+
def foo(x: Foo): Unit =
8+
x match {
9+
case Foo(x, _: _*) => assert(x == 3)
10+
}

tests/patmat/irrefutable.check

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
22: Pattern Match Exhaustivity: _: Base
2+
65: Pattern Match Exhaustivity: _: M

tests/patmat/irrefutable.scala

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
sealed trait Base
2+
sealed class A(s: String) extends Base
3+
sealed class B(val n: Int) extends Base
4+
sealed case class C(val s: String, val n: Int) extends Base
5+
6+
// boolean
7+
object ExTrue {
8+
def unapply(b: Base): true = b match {
9+
case _ => true
10+
}
11+
12+
def test(b: Base) = b match {
13+
case ExTrue() =>
14+
}
15+
}
16+
17+
object ExFalse {
18+
def unapply(b: Base): Boolean = b match {
19+
case _ => true
20+
}
21+
22+
def test(b: Base) = b match { // warning
23+
case ExFalse() =>
24+
}
25+
}
26+
27+
// product
28+
29+
object ExProduct {
30+
def unapply(b: B): (Int, Int) = (b.n, b.n * b.n)
31+
def test(b: B) = b match {
32+
case ExProduct(x, xx) =>
33+
}
34+
}
35+
36+
// isEmpty/get
37+
38+
trait Res {
39+
def isEmpty: false = false
40+
def get: Int
41+
}
42+
43+
object ExName {
44+
def unapply(b: B): Res = new Res { def get: Int = b.n }
45+
46+
def test(b: B) = b match {
47+
case ExName(x) =>
48+
}
49+
}
50+
51+
sealed class M(n: Int, s: String) extends Product {
52+
def _1: Int = n
53+
def _2: String = s
54+
def isEmpty: Boolean = s.size > n
55+
def get: M = this
56+
57+
def canEqual(that: Any): Boolean = true
58+
def productArity: Int = 2
59+
def productElement(n: Int): Any = ???
60+
}
61+
62+
object ExM {
63+
def unapply(m: M): M = m
64+
65+
def test(m: M) = m match { // warning
66+
case ExM(s) =>
67+
}
68+
}
69+
70+
// some
71+
object ExSome {
72+
def unapply(b: B): Some[Int] = Some(b.n)
73+
74+
def test(b: B) = b match {
75+
case ExSome(x) =>
76+
}
77+
}
78+
79+
object ExSome2 {
80+
def unapply(c: C): Some[C] = Some(c)
81+
82+
def test(c: C) = c match {
83+
case ExSome2(s, n) =>
84+
}
85+
}

tests/patmat/t9232.check

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
13: Pattern Match Exhaustivity: Node2()
2-
17: Pattern Match Exhaustivity: Node1(Foo(List(_, _: _*))), Node1(Foo(Nil)), Node2()
3-
21: Pattern Match Exhaustivity: Node1(Foo(Nil)), Node2()
2+
17: Pattern Match Exhaustivity: Node1(Foo(_, _: _*)), Node1(Foo()), Node2()
3+
21: Pattern Match Exhaustivity: Node1(Foo()), Node2()

tests/pos/i6849a.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
final class Foo(val value: Int)
2+
3+
object Foo {
4+
def unapplySeq(foo: Foo): Seq[Int] = List(foo.value)
5+
}
6+
7+
object Test {
8+
def main(args: Array[String]): Unit = {
9+
(new Foo(3)) match {
10+
case Foo(x, _: _*) =>
11+
assert(x == 3)
12+
}
13+
}
14+
}

tests/pos/i6849b.scala

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
def unapplySeq(a: Any): Seq[_] = ""
3+
}
4+
5+
def unapply(x: Any) = x match { case A() => }

tests/run-macros/tasty-extractors-2.check

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Unit")
4949
Inlined(None, Nil, Block(List(ClassDef("Foo", DefDef("<init>", Nil, List(Nil), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil)), Nil, None, List(DefDef("a", Nil, Nil, Inferred(), Some(Literal(Constant(0))))))), Literal(Constant(()))))
5050
TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Unit")
5151

52-
Inlined(None, Nil, Block(List(ClassDef("Foo", DefDef("<init>", Nil, List(Nil), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil), TypeSelect(Select(Ident("_root_"), "scala"), "Product"), TypeSelect(Select(Ident("_root_"), "scala"), "Serializable")), Nil, None, List(DefDef("copy", Nil, List(Nil), Inferred(), Some(Apply(Select(New(Inferred()), "<init>"), Nil))))), ValDef("Foo", TypeIdent("Foo$"), Some(Apply(Select(New(TypeIdent("Foo$")), "<init>"), Nil))), ClassDef("Foo$", DefDef("<init>", Nil, List(Nil), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil), Applied(Inferred(), List(Inferred())), TypeSelect(Select(Ident("_root_"), "scala"), "Serializable")), Nil, Some(ValDef("_", Singleton(Ident("Foo")), None)), List(DefDef("apply", Nil, List(Nil), Inferred(), Some(Apply(Select(New(Inferred()), "<init>"), Nil))), DefDef("unapply", Nil, List(List(ValDef("x$1", Inferred(), None))), Inferred(), Some(Literal(Constant(true))))))), Literal(Constant(()))))
52+
Inlined(None, Nil, Block(List(ClassDef("Foo", DefDef("<init>", Nil, List(Nil), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil), TypeSelect(Select(Ident("_root_"), "scala"), "Product"), TypeSelect(Select(Ident("_root_"), "scala"), "Serializable")), Nil, None, List(DefDef("copy", Nil, List(Nil), Inferred(), Some(Apply(Select(New(Inferred()), "<init>"), Nil))))), ValDef("Foo", TypeIdent("Foo$"), Some(Apply(Select(New(TypeIdent("Foo$")), "<init>"), Nil))), ClassDef("Foo$", DefDef("<init>", Nil, List(Nil), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil), Applied(Inferred(), List(Inferred())), TypeSelect(Select(Ident("_root_"), "scala"), "Serializable")), Nil, Some(ValDef("_", Singleton(Ident("Foo")), None)), List(DefDef("apply", Nil, List(Nil), Inferred(), Some(Apply(Select(New(Inferred()), "<init>"), Nil))), DefDef("unapply", Nil, List(List(ValDef("x$1", Inferred(), None))), Singleton(Literal(Constant(true))), Some(Literal(Constant(true))))))), Literal(Constant(()))))
5353
TypeRef(ThisType(TypeRef(NoPrefix(), "scala")), "Unit")
5454

5555
Inlined(None, Nil, Block(List(ClassDef("Foo1", DefDef("<init>", Nil, List(List(ValDef("a", TypeIdent("Int"), None))), Inferred(), None), List(Apply(Select(New(Inferred()), "<init>"), Nil)), Nil, None, List(ValDef("a", Inferred(), None)))), Literal(Constant(()))))

tests/run/i6490.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
final class Foo(val value: Int)
2+
3+
object Foo {
4+
def unapplySeq(foo: Foo): Seq[Int] = List(foo.value)
5+
}
6+
7+
object Test {
8+
def main(args: Array[String]): Unit = {
9+
(new Foo(3)) match {
10+
case Foo(x, _: _*) =>
11+
assert(x == 3)
12+
}
13+
}
14+
}

tests/run/i6490b.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
final class Foo(val value: Int)
2+
3+
object Foo {
4+
def unapplySeq(foo: Foo): (Int, Seq[Int]) = (foo.value, Nil)
5+
}
6+
7+
object Test {
8+
def main(args: Array[String]): Unit = {
9+
(new Foo(3)) match {
10+
case Foo(x, _: _*) =>
11+
assert(x == 3)
12+
}
13+
}
14+
}

tests/run/string-extractor.scala

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
final class StringExtract(val s: String) extends AnyVal {
2-
def isEmpty = (s eq null) || (s == "")
3-
def get = this
42
def length = s.length
53
def lengthCompare(n: Int) = s.length compare n
64
def apply(idx: Int): Char = s charAt idx
@@ -13,7 +11,6 @@ final class StringExtract(val s: String) extends AnyVal {
1311
}
1412

1513
final class ThreeStringExtract(val s: String) extends AnyVal {
16-
def isEmpty = (s eq null) || (s == "")
1714
def get: (List[Int], Double, Seq[Char]) = ((s.length :: Nil, s.length.toDouble, toSeq))
1815
def length = s.length
1916
def lengthCompare(n: Int) = s.length compare n
@@ -28,10 +25,14 @@ final class ThreeStringExtract(val s: String) extends AnyVal {
2825

2926

3027
object Bippy {
31-
def unapplySeq(x: Any): StringExtract = new StringExtract("" + x)
28+
def unapplySeq(x: Any): Option[StringExtract] =
29+
if ((x == null) || (x == "")) None
30+
else Some(new StringExtract("" + x))
3231
}
3332
object TripleBippy {
34-
def unapplySeq(x: Any): ThreeStringExtract = new ThreeStringExtract("" + x)
33+
def unapplySeq(x: Any): Option[(List[Int], Double, Seq[Char])] =
34+
if ((x == null) || (x == "")) then None
35+
else Some(new ThreeStringExtract("" + x).get)
3536
}
3637

3738
object Test {

0 commit comments

Comments
 (0)