Skip to content

Change case class desugaring and decouple Products and name-based-pattern-matching #1938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ testlogs/
local/
compiler/test/debug/Gen.jar

compiler/before-pickling.txt
compiler/after-pickling.txt
83 changes: 55 additions & 28 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -352,26 +352,45 @@ object desugar {
lazy val creatorExpr = New(classTypeRef, constrVparamss nestedMap refOfDef)

// Methods to add to a case class C[..](p1: T1, ..., pN: Tn)(moreParams)
// def isDefined = true
// def productArity = N
// def _1 = this.p1
// ...
// def _N = this.pN
// def copy(p1: T1 = p1: @uncheckedVariance, ...,
// pN: TN = pN: @uncheckedVariance)(moreParams) =
// new C[...](p1, ..., pN)(moreParams)
//
// Above arity 22 we also synthesize:
// def productArity = N
// def productElement(i: Int): Any = i match { ... }
//
// Note: copy default parameters need @uncheckedVariance; see
// neg/t1843-variances.scala for a test case. The test would give
// two errors without @uncheckedVariance, one of them spurious.
val caseClassMeths =
if (isCaseClass) {
def syntheticProperty(name: TermName, rhs: Tree) =
DefDef(name, Nil, Nil, TypeTree(), rhs).withMods(synthetic)
val caseClassMeths = {
def syntheticProperty(name: TermName, rhs: Tree) =
DefDef(name, Nil, Nil, TypeTree(), rhs).withMods(synthetic)
def productArity = syntheticProperty(nme.productArity, Literal(Constant(arity)))
def productElement = {
val param = makeSyntheticParameter(tpt = ref(defn.IntType))
// case N => _${N + 1}
val cases = 0.until(arity).map { i =>
CaseDef(Literal(Constant(i)), EmptyTree, Select(This(EmptyTypeIdent), nme.selectorName(i)))
}
val ioob = ref(defn.IndexOutOfBoundsException.typeRef)
val error = Throw(New(ioob, List(List(Select(refOfDef(param), nme.toString_)))))
// case _ => throw new IndexOutOfBoundsException(i.toString)
val defaultCase = CaseDef(untpd.Ident(nme.WILDCARD), EmptyTree, error)
val body = Match(refOfDef(param), (cases :+ defaultCase).toList)
DefDef(nme.productElement, Nil, List(List(param)), TypeTree(defn.AnyType), body)
.withMods(synthetic)
}
def productElemMeths = {
val caseParams = constrVparamss.head.toArray
val productElemMeths =
for (i <- 0 until arity if nme.selectorName(i) `ne` caseParams(i).name)
yield syntheticProperty(nme.selectorName(i), Select(This(EmptyTypeIdent), caseParams(i).name))
for (i <- 0 until arity if nme.selectorName(i) `ne` caseParams(i).name)
yield syntheticProperty(nme.selectorName(i), Select(This(EmptyTypeIdent), caseParams(i).name))
}
def enumTagMeths = if (isEnumCase) enumTagMeth(CaseKind.Class)._1 :: Nil else Nil
def copyMeths = {
def isRepeated(tree: Tree): Boolean = tree match {
case PostfixOp(_, Ident(nme.raw.STAR)) => true
case ByNameTypeTree(tree1) => isRepeated(tree1)
Expand All @@ -381,38 +400,46 @@ object desugar {
case ValDef(_, tpt, _) => isRepeated(tpt)
case _ => false
})

val copyMeths =
if (mods.is(Abstract) || hasRepeatedParam) Nil // cannot have default arguments for repeated parameters, hence copy method is not issued
else {
def copyDefault(vparam: ValDef) =
makeAnnotated("scala.annotation.unchecked.uncheckedVariance", refOfDef(vparam))
val copyFirstParams = derivedVparamss.head.map(vparam =>
cpy.ValDef(vparam)(rhs = copyDefault(vparam)))
val copyRestParamss = derivedVparamss.tail.nestedMap(vparam =>
cpy.ValDef(vparam)(rhs = EmptyTree))
DefDef(nme.copy, derivedTparams, copyFirstParams :: copyRestParamss, TypeTree(), creatorExpr)
.withMods(synthetic) :: Nil
}

val enumTagMeths = if (isEnumCase) enumTagMeth(CaseKind.Class)._1 :: Nil else Nil
copyMeths ::: enumTagMeths ::: productElemMeths.toList
if (mods.is(Abstract) || hasRepeatedParam) Nil // cannot have default arguments for repeated parameters, hence copy method is not issued
else {
def copyDefault(vparam: ValDef) =
makeAnnotated("scala.annotation.unchecked.uncheckedVariance", refOfDef(vparam))
val copyFirstParams = derivedVparamss.head.map(vparam =>
cpy.ValDef(vparam)(rhs = copyDefault(vparam)))
val copyRestParamss = derivedVparamss.tail.nestedMap(vparam =>
cpy.ValDef(vparam)(rhs = EmptyTree))
DefDef(nme.copy, derivedTparams, copyFirstParams :: copyRestParamss, TypeTree(), creatorExpr)
.withMods(synthetic) :: Nil
}
}

// Above MaxTupleArity we extend Product instead of ProductN, in this
// case we need to synthesise productElement & productArity.
def largeProductMeths =
if (arity > Definitions.MaxTupleArity) productElement :: productArity :: Nil
else Nil

if (isCaseClass)
largeProductMeths ::: copyMeths ::: enumTagMeths ::: productElemMeths.toList
else Nil
}

def anyRef = ref(defn.AnyRefAlias.typeRef)
def productConstr(n: Int) = {
val tycon = scalaDot((tpnme.Product.toString + n).toTypeName)
val targs = constrVparamss.head map (_.tpt)
if (targs.isEmpty) tycon else AppliedTypeTree(tycon, targs)
}
def product =
if (arity > Definitions.MaxTupleArity) scalaDot(nme.Product.toTypeName)
else productConstr(arity)

// Case classes and case objects get a ProductN parent
// Case classes and case objects get Product/ProductN parents
var parents1 = parents
if (isEnumCase && parents.isEmpty)
parents1 = enumClassTypeRef :: Nil
if (mods.is(Case) && arity <= Definitions.MaxTupleArity)
parents1 = parents1 :+ productConstr(arity) // TODO: This also adds Product0 to caes objects. Do we want that?
if (mods.is(Case))
parents1 = parents1 :+ product // TODO: This also adds Product0 to case objects. Do we want that?
if (isEnum)
parents1 = parents1 :+ ref(defn.EnumType)

Expand Down
15 changes: 3 additions & 12 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import scala.collection.{ mutable, immutable }
import PartialFunction._
import collection.mutable
import util.common.alwaysZero
import typer.Applications

object Definitions {

Expand Down Expand Up @@ -477,6 +478,7 @@ class Definitions {

lazy val JavaCloneableClass = ctx.requiredClass("java.lang.Cloneable")
lazy val NullPointerExceptionClass = ctx.requiredClass("java.lang.NullPointerException")
lazy val IndexOutOfBoundsException = ctx.requiredClass("java.lang.IndexOutOfBoundsException")
lazy val ClassClass = ctx.requiredClass("java.lang.Class")
lazy val BoxedNumberClass = ctx.requiredClass("java.lang.Number")
lazy val ThrowableClass = ctx.requiredClass("java.lang.Throwable")
Expand Down Expand Up @@ -845,18 +847,7 @@ class Definitions {
TupleType(elems.size).appliedTo(elems)
}

def isProductSubType(tp: Type)(implicit ctx: Context) =
(tp derivesFrom ProductType.symbol) && tp.baseClasses.exists(isProductClass)

def productArity(tp: Type)(implicit ctx: Context) =
if (tp derivesFrom ProductType.symbol)
tp.baseClasses.find(isProductClass) match {
case Some(prod) => prod.typeParams.length
case None => -1
}
else -1

/** Is `tp` (an alias) of either a scala.FunctionN or a scala.ImplicitFunctionN ? */
/** Is `tp` (an alias) of either a scala.FunctionN or a scala.ImplicitFunctionN? */
def isFunctionType(tp: Type)(implicit ctx: Context) = {
val arity = functionArity(tp)
val sym = tp.dealias.typeSymbol
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package ammonite.terminal

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

sealed trait TermAction
trait TermAction
case class Printing(ts: TermState, stdout: String) extends TermAction
case class TermState(
inputs: LazyList[Int],
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {
// next: MatchMonad[U]
// returns MatchMonad[U]
def flatMap(prev: Tree, b: Symbol, next: Tree): Tree = {
val resultArity = defn.productArity(b.info)
val resultArity = productArity(b.info)
if (isProductMatch(prev.tpe, resultArity)) {
val nullCheck: Tree = prev.select(defn.Object_ne).appliedTo(Literal(Constant(null)))
ifThenElseZero(
Expand Down Expand Up @@ -1408,7 +1408,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {
protected def seqTree(binder: Symbol) = tupleSel(binder)(firstIndexingBinder + 1)
protected def tupleSel(binder: Symbol)(i: Int): Tree = {
val accessors =
if (defn.isProductSubType(binder.info))
if (Applications.canProductMatch(binder.info))
productSelectors(binder.info)
else binder.caseAccessors
val res =
Expand Down
15 changes: 10 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ object Applications {
ref.info.widenExpr.dealias
}

def canProductMatch(tp: Type)(implicit ctx: Context) =
extractorMemberType(tp, nme._1).exists

/** Does `tp` fit the "product match" conditions as an unapply result type
* for a pattern with `numArgs` subpatterns>
* This is the case of `tp` is a subtype of the Product<numArgs> class.
* for a pattern with `numArgs` subpatterns?
* This is the case of `tp` has members `_1` to `_N` where `N == numArgs`.
*/
def isProductMatch(tp: Type, numArgs: Int)(implicit ctx: Context) =
0 <= numArgs && numArgs <= Definitions.MaxTupleArity &&
tp.derivesFrom(defn.ProductNType(numArgs).typeSymbol)
numArgs > 0 && productArity(tp) == numArgs

/** Does `tp` fit the "get match" conditions as an unapply result type?
* This is the case of `tp` has a `get` member as well as a
Expand All @@ -68,6 +70,9 @@ object Applications {
sels.takeWhile(_.exists).toList
}

def productArity(tp: Type)(implicit ctx: Context) =
if (canProductMatch(tp)) productSelectorTypes(tp).size else -1

def productSelectors(tp: Type)(implicit ctx: Context): List[Symbol] = {
val sels = for (n <- Iterator.from(0)) yield tp.member(nme.selectorName(n)).symbol
sels.takeWhile(_.exists).toList
Expand Down Expand Up @@ -108,7 +113,7 @@ object Applications {
getUnapplySelectors(getTp, args, pos)
else if (unapplyResult isRef defn.BooleanClass)
Nil
else if (defn.isProductSubType(unapplyResult))
else if (canProductMatch(unapplyResult))
productSelectorTypes(unapplyResult)
// this will cause a "wrong number of arguments in pattern" error later on,
// which is better than the message in `fail`.
Expand Down
5 changes: 2 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -761,10 +761,9 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit

/** Is `formal` a product type which is elementwise compatible with `params`? */
def ptIsCorrectProduct(formal: Type) = {
val pclass = defn.ProductNType(params.length).symbol
isFullyDefined(formal, ForceDegree.noBottom) &&
formal.derivesFrom(pclass) &&
formal.baseArgTypes(pclass).corresponds(params) {
Applications.canProductMatch(formal) &&
Applications.productSelectorTypes(formal).corresponds(params) {
(argType, param) =>
param.tpt.isEmpty || argType <:< typedAheadType(param.tpt).tpe
}
Expand Down
File renamed without changes.
45 changes: 45 additions & 0 deletions tests/run/1938.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
case class Large(
e1: Int,
e2: Int,
e3: Int,
e4: Int,
e5: Int,
e6: Int,
e7: Int,
e8: Int,
e9: Int,
e10: Int,
e11: Int,
e12: Int,
e13: Int,
e14: Int,
e15: Int,
e16: Int,
e17: Int,
e18: Int,
e19: Int,
e20: Int,
e21: Int,
e22: Int,
e23: Int
)

object Test {
def main(args: Array[String]): Unit = {
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)

assert(l.productArity == 23)

assert(l.productElement(0) == 1)
assert(l.productElement(1) == 2)
assert(l.productElement(21) == 22)
assert(l.productElement(22) == 23)

try {
l.productElement(23)
???
} catch {
case e: IndexOutOfBoundsException => assert(e.getMessage == "23")
}
}
}
File renamed without changes.
File renamed without changes.
40 changes: 40 additions & 0 deletions tests/run/double-pattern-type.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
case class C1(i: String, s: Int) { def isEmpty = false; def get = ("EMPTY", -1) }
case class C2(i: String, s: String) { def isEmpty = false; def get = (-1, -2, -3) }

object Test {
def main(args: Array[String]): Unit = {
// When both Product and name based patterns with same arity are available,
// we follow scalac and silently use the Product one:

val c1 = C1("s", 0)
c1 match {
case C1(a, b) =>
assert(a == "s")
assert(b == 0)
}

// When the size differ, both are patterns become usable:

val c2 = C2("a", "b")
c2 match {
case C2(a, b) =>
assert(a == "a")
assert(b == "b")
}

c2 match {
case C2(a, b, c) =>
assert(a == -1)
assert(b == -2)
assert(c == -3)
}

// Interestingly things also compile with a single pattern, in which case
// the tuple returned by get is binded to `a`:

c2 match {
case C2(a) =>
assert(a == (-1, -2, -3))
}
}
}
26 changes: 26 additions & 0 deletions tests/run/zero-arity-case-class.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
case class Foo()

object Test {
def main(args: Array[String]): Unit = {
assert(Foo.unapply(Foo()) == true)

// unapply generate by scalac are `_ != null`,
// dotty returns true in all cases
assert(Foo.unapply(null) == true)

Foo() match {
case Foo() => ()
case _ => ???
}

Foo() match {
case _: Foo => ()
case _ => ???
}

(Foo(): Any) match {
case Foo() => ()
case _ => ???
}
}
}