Skip to content

Commit 4868fb2

Browse files
authored
Merge pull request #1938 from dotty-staging/named-based-patmat
Change case class desugaring and decouple Products and name-based-pattern-matching
2 parents 579571e + 198b5ce commit 4868fb2

13 files changed

+186
-51
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ testlogs/
5555
local/
5656
compiler/test/debug/Gen.jar
5757

58+
compiler/before-pickling.txt
59+
compiler/after-pickling.txt

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

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -353,26 +353,45 @@ object desugar {
353353
lazy val creatorExpr = New(classTypeRef, constrVparamss nestedMap refOfDef)
354354

355355
// Methods to add to a case class C[..](p1: T1, ..., pN: Tn)(moreParams)
356-
// def isDefined = true
357-
// def productArity = N
358356
// def _1 = this.p1
359357
// ...
360358
// def _N = this.pN
361359
// def copy(p1: T1 = p1: @uncheckedVariance, ...,
362360
// pN: TN = pN: @uncheckedVariance)(moreParams) =
363361
// new C[...](p1, ..., pN)(moreParams)
364362
//
363+
// Above arity 22 we also synthesize:
364+
// def productArity = N
365+
// def productElement(i: Int): Any = i match { ... }
366+
//
365367
// Note: copy default parameters need @uncheckedVariance; see
366368
// neg/t1843-variances.scala for a test case. The test would give
367369
// two errors without @uncheckedVariance, one of them spurious.
368-
val caseClassMeths =
369-
if (isCaseClass) {
370-
def syntheticProperty(name: TermName, rhs: Tree) =
371-
DefDef(name, Nil, Nil, TypeTree(), rhs).withMods(synthetic)
370+
val caseClassMeths = {
371+
def syntheticProperty(name: TermName, rhs: Tree) =
372+
DefDef(name, Nil, Nil, TypeTree(), rhs).withMods(synthetic)
373+
def productArity = syntheticProperty(nme.productArity, Literal(Constant(arity)))
374+
def productElement = {
375+
val param = makeSyntheticParameter(tpt = ref(defn.IntType))
376+
// case N => _${N + 1}
377+
val cases = 0.until(arity).map { i =>
378+
CaseDef(Literal(Constant(i)), EmptyTree, Select(This(EmptyTypeIdent), nme.selectorName(i)))
379+
}
380+
val ioob = ref(defn.IndexOutOfBoundsException.typeRef)
381+
val error = Throw(New(ioob, List(List(Select(refOfDef(param), nme.toString_)))))
382+
// case _ => throw new IndexOutOfBoundsException(i.toString)
383+
val defaultCase = CaseDef(untpd.Ident(nme.WILDCARD), EmptyTree, error)
384+
val body = Match(refOfDef(param), (cases :+ defaultCase).toList)
385+
DefDef(nme.productElement, Nil, List(List(param)), TypeTree(defn.AnyType), body)
386+
.withMods(synthetic)
387+
}
388+
def productElemMeths = {
372389
val caseParams = constrVparamss.head.toArray
373-
val productElemMeths =
374-
for (i <- 0 until arity if nme.selectorName(i) `ne` caseParams(i).name)
375-
yield syntheticProperty(nme.selectorName(i), Select(This(EmptyTypeIdent), caseParams(i).name))
390+
for (i <- 0 until arity if nme.selectorName(i) `ne` caseParams(i).name)
391+
yield syntheticProperty(nme.selectorName(i), Select(This(EmptyTypeIdent), caseParams(i).name))
392+
}
393+
def enumTagMeths = if (isEnumCase) enumTagMeth(CaseKind.Class)._1 :: Nil else Nil
394+
def copyMeths = {
376395
def isRepeated(tree: Tree): Boolean = tree match {
377396
case PostfixOp(_, Ident(nme.raw.STAR)) => true
378397
case ByNameTypeTree(tree1) => isRepeated(tree1)
@@ -382,38 +401,46 @@ object desugar {
382401
case ValDef(_, tpt, _) => isRepeated(tpt)
383402
case _ => false
384403
})
385-
386-
val copyMeths =
387-
if (mods.is(Abstract) || hasRepeatedParam) Nil // cannot have default arguments for repeated parameters, hence copy method is not issued
388-
else {
389-
def copyDefault(vparam: ValDef) =
390-
makeAnnotated("scala.annotation.unchecked.uncheckedVariance", refOfDef(vparam))
391-
val copyFirstParams = derivedVparamss.head.map(vparam =>
392-
cpy.ValDef(vparam)(rhs = copyDefault(vparam)))
393-
val copyRestParamss = derivedVparamss.tail.nestedMap(vparam =>
394-
cpy.ValDef(vparam)(rhs = EmptyTree))
395-
DefDef(nme.copy, derivedTparams, copyFirstParams :: copyRestParamss, TypeTree(), creatorExpr)
396-
.withMods(synthetic) :: Nil
397-
}
398-
399-
val enumTagMeths = if (isEnumCase) enumTagMeth(CaseKind.Class)._1 :: Nil else Nil
400-
copyMeths ::: enumTagMeths ::: productElemMeths.toList
404+
if (mods.is(Abstract) || hasRepeatedParam) Nil // cannot have default arguments for repeated parameters, hence copy method is not issued
405+
else {
406+
def copyDefault(vparam: ValDef) =
407+
makeAnnotated("scala.annotation.unchecked.uncheckedVariance", refOfDef(vparam))
408+
val copyFirstParams = derivedVparamss.head.map(vparam =>
409+
cpy.ValDef(vparam)(rhs = copyDefault(vparam)))
410+
val copyRestParamss = derivedVparamss.tail.nestedMap(vparam =>
411+
cpy.ValDef(vparam)(rhs = EmptyTree))
412+
DefDef(nme.copy, derivedTparams, copyFirstParams :: copyRestParamss, TypeTree(), creatorExpr)
413+
.withMods(synthetic) :: Nil
414+
}
401415
}
416+
417+
// Above MaxTupleArity we extend Product instead of ProductN, in this
418+
// case we need to synthesise productElement & productArity.
419+
def largeProductMeths =
420+
if (arity > Definitions.MaxTupleArity) productElement :: productArity :: Nil
421+
else Nil
422+
423+
if (isCaseClass)
424+
largeProductMeths ::: copyMeths ::: enumTagMeths ::: productElemMeths.toList
402425
else Nil
426+
}
403427

404428
def anyRef = ref(defn.AnyRefAlias.typeRef)
405429
def productConstr(n: Int) = {
406430
val tycon = scalaDot((str.Product + n).toTypeName)
407431
val targs = constrVparamss.head map (_.tpt)
408432
if (targs.isEmpty) tycon else AppliedTypeTree(tycon, targs)
409433
}
434+
def product =
435+
if (arity > Definitions.MaxTupleArity) scalaDot(nme.Product.toTypeName)
436+
else productConstr(arity)
410437

411-
// Case classes and case objects get a ProductN parent
438+
// Case classes and case objects get Product/ProductN parents
412439
var parents1 = parents
413440
if (isEnumCase && parents.isEmpty)
414441
parents1 = enumClassTypeRef :: Nil
415-
if (mods.is(Case) && arity <= Definitions.MaxTupleArity)
416-
parents1 = parents1 :+ productConstr(arity) // TODO: This also adds Product0 to caes objects. Do we want that?
442+
if (mods.is(Case))
443+
parents1 = parents1 :+ product // TODO: This also adds Product0 to case objects. Do we want that?
417444
if (isEnum)
418445
parents1 = parents1 :+ ref(defn.EnumType)
419446

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import scala.collection.{ mutable, immutable }
1010
import PartialFunction._
1111
import collection.mutable
1212
import util.common.alwaysZero
13+
import typer.Applications
1314

1415
object Definitions {
1516

@@ -477,6 +478,7 @@ class Definitions {
477478

478479
lazy val JavaCloneableClass = ctx.requiredClass("java.lang.Cloneable")
479480
lazy val NullPointerExceptionClass = ctx.requiredClass("java.lang.NullPointerException")
481+
lazy val IndexOutOfBoundsException = ctx.requiredClass("java.lang.IndexOutOfBoundsException")
480482
lazy val ClassClass = ctx.requiredClass("java.lang.Class")
481483
lazy val BoxedNumberClass = ctx.requiredClass("java.lang.Number")
482484
lazy val ThrowableClass = ctx.requiredClass("java.lang.Throwable")
@@ -844,18 +846,7 @@ class Definitions {
844846
TupleType(elems.size).appliedTo(elems)
845847
}
846848

847-
def isProductSubType(tp: Type)(implicit ctx: Context) =
848-
(tp derivesFrom ProductType.symbol) && tp.baseClasses.exists(isProductClass)
849-
850-
def productArity(tp: Type)(implicit ctx: Context) =
851-
if (tp derivesFrom ProductType.symbol)
852-
tp.baseClasses.find(isProductClass) match {
853-
case Some(prod) => prod.typeParams.length
854-
case None => -1
855-
}
856-
else -1
857-
858-
/** Is `tp` (an alias) of either a scala.FunctionN or a scala.ImplicitFunctionN ? */
849+
/** Is `tp` (an alias) of either a scala.FunctionN or a scala.ImplicitFunctionN? */
859850
def isFunctionType(tp: Type)(implicit ctx: Context) = {
860851
val arity = functionArity(tp)
861852
val sym = tp.dealias.typeSymbol

compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package ammonite.terminal
55

66
case class TermInfo(ts: TermState, width: Int)
77

8-
sealed trait TermAction
8+
trait TermAction
99
case class Printing(ts: TermState, stdout: String) extends TermAction
1010
case class TermState(
1111
inputs: LazyList[Int],

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {
233233
// next: MatchMonad[U]
234234
// returns MatchMonad[U]
235235
def flatMap(prev: Tree, b: Symbol, next: Tree): Tree = {
236-
val resultArity = defn.productArity(b.info)
236+
val resultArity = productArity(b.info)
237237
if (isProductMatch(prev.tpe, resultArity)) {
238238
val nullCheck: Tree = prev.select(defn.Object_ne).appliedTo(Literal(Constant(null)))
239239
ifThenElseZero(
@@ -1408,7 +1408,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {
14081408
protected def seqTree(binder: Symbol) = tupleSel(binder)(firstIndexingBinder + 1)
14091409
protected def tupleSel(binder: Symbol)(i: Int): Tree = {
14101410
val accessors =
1411-
if (defn.isProductSubType(binder.info))
1411+
if (Applications.canProductMatch(binder.info))
14121412
productSelectors(binder.info)
14131413
else binder.caseAccessors
14141414
val res =

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ object Applications {
4848
ref.info.widenExpr.dealias
4949
}
5050

51+
def canProductMatch(tp: Type)(implicit ctx: Context) =
52+
extractorMemberType(tp, nme._1).exists
53+
5154
/** Does `tp` fit the "product match" conditions as an unapply result type
52-
* for a pattern with `numArgs` subpatterns>
53-
* This is the case of `tp` is a subtype of the Product<numArgs> class.
55+
* for a pattern with `numArgs` subpatterns?
56+
* This is the case of `tp` has members `_1` to `_N` where `N == numArgs`.
5457
*/
5558
def isProductMatch(tp: Type, numArgs: Int)(implicit ctx: Context) =
56-
0 <= numArgs && numArgs <= Definitions.MaxTupleArity &&
57-
tp.derivesFrom(defn.ProductNType(numArgs).typeSymbol)
59+
numArgs > 0 && productArity(tp) == numArgs
5860

5961
/** Does `tp` fit the "get match" conditions as an unapply result type?
6062
* This is the case of `tp` has a `get` member as well as a
@@ -69,6 +71,9 @@ object Applications {
6971
sels.takeWhile(_.exists).toList
7072
}
7173

74+
def productArity(tp: Type)(implicit ctx: Context) =
75+
if (canProductMatch(tp)) productSelectorTypes(tp).size else -1
76+
7277
def productSelectors(tp: Type)(implicit ctx: Context): List[Symbol] = {
7378
val sels = for (n <- Iterator.from(0)) yield tp.member(nme.selectorName(n)).symbol
7479
sels.takeWhile(_.exists).toList
@@ -109,7 +114,7 @@ object Applications {
109114
getUnapplySelectors(getTp, args, pos)
110115
else if (unapplyResult isRef defn.BooleanClass)
111116
Nil
112-
else if (defn.isProductSubType(unapplyResult))
117+
else if (canProductMatch(unapplyResult))
113118
productSelectorTypes(unapplyResult)
114119
// this will cause a "wrong number of arguments in pattern" error later on,
115120
// which is better than the message in `fail`.

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -762,10 +762,9 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
762762

763763
/** Is `formal` a product type which is elementwise compatible with `params`? */
764764
def ptIsCorrectProduct(formal: Type) = {
765-
val pclass = defn.ProductNType(params.length).symbol
766765
isFullyDefined(formal, ForceDegree.noBottom) &&
767-
formal.derivesFrom(pclass) &&
768-
formal.baseArgTypes(pclass).corresponds(params) {
766+
Applications.canProductMatch(formal) &&
767+
Applications.productSelectorTypes(formal).corresponds(params) {
769768
(argType, param) =>
770769
param.tpt.isEmpty || argType <:< typedAheadType(param.tpt).tpe
771770
}
File renamed without changes.

tests/run/1938.scala

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
case class Large(
2+
e1: Int,
3+
e2: Int,
4+
e3: Int,
5+
e4: Int,
6+
e5: Int,
7+
e6: Int,
8+
e7: Int,
9+
e8: Int,
10+
e9: Int,
11+
e10: Int,
12+
e11: Int,
13+
e12: Int,
14+
e13: Int,
15+
e14: Int,
16+
e15: Int,
17+
e16: Int,
18+
e17: Int,
19+
e18: Int,
20+
e19: Int,
21+
e20: Int,
22+
e21: Int,
23+
e22: Int,
24+
e23: Int
25+
)
26+
27+
object Test {
28+
def main(args: Array[String]): Unit = {
29+
val l = Large(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
30+
31+
assert(l.productArity == 23)
32+
33+
assert(l.productElement(0) == 1)
34+
assert(l.productElement(1) == 2)
35+
assert(l.productElement(21) == 22)
36+
assert(l.productElement(22) == 23)
37+
38+
try {
39+
l.productElement(23)
40+
???
41+
} catch {
42+
case e: IndexOutOfBoundsException => assert(e.getMessage == "23")
43+
}
44+
}
45+
}
File renamed without changes.
File renamed without changes.

tests/run/double-pattern-type.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
case class C1(i: String, s: Int) { def isEmpty = false; def get = ("EMPTY", -1) }
2+
case class C2(i: String, s: String) { def isEmpty = false; def get = (-1, -2, -3) }
3+
4+
object Test {
5+
def main(args: Array[String]): Unit = {
6+
// When both Product and name based patterns with same arity are available,
7+
// we follow scalac and silently use the Product one:
8+
9+
val c1 = C1("s", 0)
10+
c1 match {
11+
case C1(a, b) =>
12+
assert(a == "s")
13+
assert(b == 0)
14+
}
15+
16+
// When the size differ, both are patterns become usable:
17+
18+
val c2 = C2("a", "b")
19+
c2 match {
20+
case C2(a, b) =>
21+
assert(a == "a")
22+
assert(b == "b")
23+
}
24+
25+
c2 match {
26+
case C2(a, b, c) =>
27+
assert(a == -1)
28+
assert(b == -2)
29+
assert(c == -3)
30+
}
31+
32+
// Interestingly things also compile with a single pattern, in which case
33+
// the tuple returned by get is binded to `a`:
34+
35+
c2 match {
36+
case C2(a) =>
37+
assert(a == (-1, -2, -3))
38+
}
39+
}
40+
}

tests/run/zero-arity-case-class.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
case class Foo()
2+
3+
object Test {
4+
def main(args: Array[String]): Unit = {
5+
assert(Foo.unapply(Foo()) == true)
6+
7+
// unapply generate by scalac are `_ != null`,
8+
// dotty returns true in all cases
9+
assert(Foo.unapply(null) == true)
10+
11+
Foo() match {
12+
case Foo() => ()
13+
case _ => ???
14+
}
15+
16+
Foo() match {
17+
case _: Foo => ()
18+
case _ => ???
19+
}
20+
21+
(Foo(): Any) match {
22+
case Foo() => ()
23+
case _ => ???
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)