Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 4 additions & 10 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 @@ -846,17 +848,9 @@ class Definitions {
}

def isProductSubType(tp: Type)(implicit ctx: Context) =
(tp derivesFrom ProductType.symbol) && tp.baseClasses.exists(isProductClass)
Applications.extractorMemberType(tp, nme._1).exists
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isProductSubType method should be renamed and moved to Applications.


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
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
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ object Applications {
}

/** 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 && defn.isProductSubType(tp) &&
productSelectorTypes(tp).size == numArgs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use productArity instead?


/** 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 +68,9 @@ object Applications {
sels.takeWhile(_.exists).toList
}

def productArity(tp: Type)(implicit ctx: Context) =
if (defn.isProductSubType(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
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) {
defn.isProductSubType(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 _ => ???
}
}
}