diff --git a/.gitignore b/.gitignore index 170cf4823dd9..7508b4932d95 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ testlogs/ local/ compiler/test/debug/Gen.jar +compiler/before-pickling.txt +compiler/after-pickling.txt diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 66fc6bf84276..45b3397276ff 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -352,8 +352,6 @@ 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 @@ -361,17 +359,38 @@ object desugar { // 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) @@ -381,24 +400,29 @@ 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) = { @@ -406,13 +430,16 @@ object desugar { 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) diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index b70fcb093ff3..8ff6a2ea931e 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -10,6 +10,7 @@ import scala.collection.{ mutable, immutable } import PartialFunction._ import collection.mutable import util.common.alwaysZero +import typer.Applications object Definitions { @@ -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") @@ -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 diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala index 34d31aecada9..e066050b68cc 100644 --- a/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala @@ -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], diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index dbc7666f77f7..781bc1bec29f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -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( @@ -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 = diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 4e43e429be92..433a82d3ec84 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -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 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 @@ -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 @@ -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`. diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 2760ceba9b48..9494bf4eb402 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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 } diff --git a/tests/pending/pos/t7296.scala b/tests/pos/t7296.scala similarity index 100% rename from tests/pending/pos/t7296.scala rename to tests/pos/t7296.scala diff --git a/tests/run/1938.scala b/tests/run/1938.scala new file mode 100644 index 000000000000..95e94678d988 --- /dev/null +++ b/tests/run/1938.scala @@ -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") + } + } +} diff --git a/tests/pending/run/case-class-23.check b/tests/run/case-class-23.check similarity index 100% rename from tests/pending/run/case-class-23.check rename to tests/run/case-class-23.check diff --git a/tests/pending/run/case-class-23.scala b/tests/run/case-class-23.scala similarity index 100% rename from tests/pending/run/case-class-23.scala rename to tests/run/case-class-23.scala diff --git a/tests/run/double-pattern-type.scala b/tests/run/double-pattern-type.scala new file mode 100644 index 000000000000..8045d173b47f --- /dev/null +++ b/tests/run/double-pattern-type.scala @@ -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)) + } + } +} diff --git a/tests/run/zero-arity-case-class.scala b/tests/run/zero-arity-case-class.scala new file mode 100644 index 000000000000..de0ba4fe1d14 --- /dev/null +++ b/tests/run/zero-arity-case-class.scala @@ -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 _ => ??? + } + } +}