From f1e553ecfbea25bcc7e13294f6104d599336f460 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Sat, 29 Aug 2015 21:42:29 +1000 Subject: SI-9029 Fix regression in extractor patterns The unified treatment of classical and named-based pattern matching does not correctly handle the generalization of "tuple capture". By "tuple capture", I mean: ``` scala> object Extractor { def unapply(a: Any): Option[(Int, String)] = Some((1, "2")) } defined object Extractor scala> "" match { case Extractor(x: Int, y: String) => } scala> "" match { case Extractor(xy : (Int, String)) => } warning: there was one deprecation warning; re-run with -deprecation for details scala> :warnings :9: warning: object Extractor expects 2 patterns to hold (Int, String) but crushing into 2-tuple to fit single pattern (SI-6675) "" match { case Extractor(xy : (Int, String)) => } ^ ``` Name based pattern matching, new in Scala 2.11, allows one to deconstruct the elements that structurally resembles `ProductN`: ``` scala> class P2(val _1: Int, val _2: String) defined class P2 scala> object Extractor { def unapply(a: Any): Option[P2] = Some(new P2(1, "2")) } defined object Extractor scala> "" match { case Extractor(x: Int, y: String) => } ``` However, attempting to extract the `P2` in its entirety leads to an internal error: ``` scala> "" match { case Extractor(p2: P2) => } :10: warning: fruitless type test: a value of type (Int, String) cannot also be a P2 "" match { case Extractor(p2: P2) => } ^ :10: error: error during expansion of this match (this is a scalac bug). The underlying error was: type mismatch; found : P2 required: (Int, String) "" match { case Extractor(p2: P2) => } ^ ``` Note that this match was legal and warning free in 2.10. This commit avoids the hard-coded assumption that the "tuple capture" results in a `TupleN`, and instead keeps track of the product-ish type from which we extracted the element types. I have also opted not to limit the deprecation warning to `TupleN` extractors. --- .../nsc/transform/patmat/PatternExpander.scala | 18 +++++++++++- .../transform/patmat/ScalacPatternExpanders.scala | 34 ++++++++++++---------- test/files/run/t9029.flags | 1 + test/files/run/t9029.scala | 15 ++++++++++ test/files/run/t9029b.check | 1 + test/files/run/t9029b.scala | 31 ++++++++++++++++++++ test/files/run/t9029c.scala | 21 +++++++++++++ 7 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 test/files/run/t9029.flags create mode 100644 test/files/run/t9029.scala create mode 100644 test/files/run/t9029b.check create mode 100644 test/files/run/t9029b.scala create mode 100644 test/files/run/t9029c.scala diff --git a/src/compiler/scala/tools/nsc/transform/patmat/PatternExpander.scala b/src/compiler/scala/tools/nsc/transform/patmat/PatternExpander.scala index e84ccbf754..1916050dd8 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/PatternExpander.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/PatternExpander.scala @@ -86,9 +86,25 @@ trait PatternExpander[Pattern, Type] { * @param fixed The non-sequence types which are extracted * @param repeated The sequence type which is extracted */ - final case class Extractor(whole: Type, fixed: List[Type], repeated: Repeated) { + final case class Extractor(whole: Type, fixed: List[Type], repeated: Repeated, typeOfSinglePattern: Type) { require(whole != NoType, s"expandTypes($whole, $fixed, $repeated)") + /** A pattern with arity-1 that doesn't match the arity of the Product-like result of the `get` method, + * will match that result in its entirety. Example: + * + * {{{ + * warning: there was one deprecation warning; re-run with -deprecation for details + * scala> object Extractor { def unapply(a: Any): Option[(Int, String)] = Some((1, "2")) } + * defined object Extractor + * + * scala> "" match { case Extractor(x: Int, y: String) => } + * + * scala> "" match { case Extractor(xy : (Int, String)) => } + * warning: there was one deprecation warning; re-run with -deprecation for details + * }}} + * */ + def asSinglePattern: Extractor = copy(fixed = List(typeOfSinglePattern)) + def productArity = fixed.length def hasSeq = repeated.exists def elementType = repeated.elementType diff --git a/src/compiler/scala/tools/nsc/transform/patmat/ScalacPatternExpanders.scala b/src/compiler/scala/tools/nsc/transform/patmat/ScalacPatternExpanders.scala index b1783dc81f..55d4366a46 100644 --- a/src/compiler/scala/tools/nsc/transform/patmat/ScalacPatternExpanders.scala +++ b/src/compiler/scala/tools/nsc/transform/patmat/ScalacPatternExpanders.scala @@ -43,8 +43,9 @@ trait ScalacPatternExpanders { orElse definitions.elementType(ArrayClass, seq) ) } - def newExtractor(whole: Type, fixed: List[Type], repeated: Repeated): Extractor = - logResult(s"newExtractor($whole, $fixed, $repeated")(Extractor(whole, fixed, repeated)) + def newExtractor(whole: Type, fixed: List[Type], repeated: Repeated, typeOfSinglePattern: Type): Extractor = + logResult(s"newExtractor($whole, $fixed, $repeated, $typeOfSinglePattern")(Extractor(whole, fixed, repeated, typeOfSinglePattern)) + def newExtractor(whole: Type, fixed: List[Type], repeated: Repeated): Extractor = newExtractor(whole, fixed, repeated, tupleType(fixed)) // Turn Seq[A] into Repeated(Seq[A], A, A*) def repeatedFromSeq(seqType: Type): Repeated = { @@ -74,16 +75,17 @@ trait ScalacPatternExpanders { * it was unapplySeq, so we have to funnel that information in separately. */ def unapplyMethodTypes(whole: Type, result: Type, isSeq: Boolean): Extractor = { - val expanded = ( - if (result =:= BooleanTpe) Nil - else typeOfMemberNamedGet(result) match { + if (result =:= BooleanTpe) newExtractor(whole, Nil, NoRepeated) + else { + val getResult = typeOfMemberNamedGet(result) + val expanded = getResult match { case rawGet if !hasSelectors(rawGet) => rawGet :: Nil case rawGet => typesOfSelectors(rawGet) } - ) - expanded match { - case init :+ last if isSeq => newExtractor(whole, init, repeatedFromSeq(last)) - case tps => newExtractor(whole, tps, NoRepeated) + expanded match { + case init :+ last if isSeq => newExtractor(whole, init, repeatedFromSeq(last), getResult) + case tps => newExtractor(whole, tps, NoRepeated, getResult) + } } } } @@ -142,12 +144,14 @@ trait ScalacPatternExpanders { def acceptMessage = if (extractor.isErroneous) "" else s" to hold ${extractor.offeringString}" val requiresTupling = isUnapply && patterns.totalArity == 1 && productArity > 1 - if (requiresTupling && effectivePatternArity(args) == 1) { - val sym = sel.symbol.owner - currentRun.reporting.deprecationWarning(sel.pos, sym, s"${sym} expects $productArity patterns$acceptMessage but crushing into $productArity-tuple to fit single pattern (SI-6675)") - } - - val normalizedExtractor = if (requiresTupling) tupleExtractor(extractor) else extractor + val normalizedExtractor = if (requiresTupling) { + val tupled = extractor.asSinglePattern + if (effectivePatternArity(args) == 1 && isTupleType(extractor.typeOfSinglePattern)) { + val sym = sel.symbol.owner + currentRun.reporting.deprecationWarning(sel.pos, sym, s"${sym} expects $productArity patterns$acceptMessage but crushing into $productArity-tuple to fit single pattern (SI-6675)") + } + tupled + } else extractor validateAligned(context, fn, Aligned(patterns, normalizedExtractor)) } diff --git a/test/files/run/t9029.flags b/test/files/run/t9029.flags new file mode 100644 index 0000000000..dcc59ebe32 --- /dev/null +++ b/test/files/run/t9029.flags @@ -0,0 +1 @@ +-deprecation diff --git a/test/files/run/t9029.scala b/test/files/run/t9029.scala new file mode 100644 index 0000000000..c01033b76e --- /dev/null +++ b/test/files/run/t9029.scala @@ -0,0 +1,15 @@ +class Y(val _2: Int, val _1: String) + +object X { def unapply(u: Unit): Option[Y] = Some(new Y(42, "!")) } + +object Test { + def test1 = { + val X(y) = () + val yy: Y = y + assert(yy._1 == "!") + assert(yy._2 == 42) + } + def main(args: Array[String]): Unit = { + test1 + } +} diff --git a/test/files/run/t9029b.check b/test/files/run/t9029b.check new file mode 100644 index 0000000000..aeb2d5e239 --- /dev/null +++ b/test/files/run/t9029b.check @@ -0,0 +1 @@ +Some(1) diff --git a/test/files/run/t9029b.scala b/test/files/run/t9029b.scala new file mode 100644 index 0000000000..764d0771ec --- /dev/null +++ b/test/files/run/t9029b.scala @@ -0,0 +1,31 @@ +class Foo(val x: Bar) { + def isEmpty = false + def get = x +} + +object Foo { + def unapply(x: Foo) = x +} + +class Bar(val x: Option[Int], val y: Option[Int]) { + def isEmpty = false + def get = this + def _1 = x + def _2 = y +} + +object Bar { + def unapply(x: Bar) = x +} + +object Test { + def nameBased: Unit = { + val x: AnyRef = new Foo(new Bar(Some(1), Some(2))) + x match { + case Foo(Bar(x1, x2)) => println(x1) + } + } + def main(args: Array[String]): Unit = { + nameBased + } +} diff --git a/test/files/run/t9029c.scala b/test/files/run/t9029c.scala new file mode 100644 index 0000000000..ccb51e23ae --- /dev/null +++ b/test/files/run/t9029c.scala @@ -0,0 +1,21 @@ +object Extractor { + def unapply(a: Any): Option[Product2[Int, String]] = Some(new P2(1, "2")) +} +class P2[A, B](val _1: A, val _2: B) extends Product2[A, B] { + def canEqual(other: Any) = true + def isP2 = true +} + +object Test { + def main(args: Array[String]): Unit = { + "" match { + case Extractor(p) => + val pp: Product2[Int, String] = p + } + "" match { + case Extractor(x, y) => + val xx: Int = x + val yy: String = y + } + } +} -- cgit v1.2.3