From 894b027ddf1551317ecdbda053aec90b6d6fa2f4 Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Sun, 26 Feb 2017 16:01:37 -0800 Subject: Allow user-defined `[un]apply` in case companion Don't emit a synthetic `apply` (or `unapply`) when it would clash with an existing one. This allows e.g., a `private apply`, along with a `case class` with a `private` constructor. We have to retract the synthetic method in a pretty roundabout way, as we need the other methods and the owner to be completed already. Unless we have to complete the synthetic `apply` while completing the user-defined one, this should not be a problem. If this does happen, this implies there's a cycle in computing the user-defined signature and the synthetic one, which is not allowed. --- .../scala/tools/nsc/typechecker/Namers.scala | 81 ++++++++++++++++++---- test/files/neg/userdefined_apply.check | 13 ++++ test/files/neg/userdefined_apply.scala | 31 +++++++++ test/files/pos/userdefined_apply.scala | 36 ++++++++++ 4 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 test/files/neg/userdefined_apply.check create mode 100644 test/files/neg/userdefined_apply.scala create mode 100644 test/files/pos/userdefined_apply.scala diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index ee64a6646f..19607b6681 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -610,7 +610,15 @@ trait Namers extends MethodSynthesis { noDuplicates(selectors map (_.rename), AppearsTwice) } - def enterCopyMethod(copyDef: DefDef): Symbol = { + class CompleterWrapper(completer: TypeCompleter) extends TypeCompleter { + val tree = completer.tree + + override def complete(sym: Symbol): Unit = { + completer.complete(sym) + } + } + + def copyMethodCompleter(copyDef: DefDef): TypeCompleter = { val sym = copyDef.symbol val lazyType = completerOf(copyDef) @@ -629,14 +637,57 @@ trait Namers extends MethodSynthesis { ) } - sym setInfo { - mkTypeCompleter(copyDef) { sym => - assignParamTypes() - lazyType complete sym - } + mkTypeCompleter(copyDef) { sym => + assignParamTypes() + lazyType complete sym } } + // for apply/unapply, which may need to disappear when they clash with a user-defined method of matching signature + def applyUnapplyMethodCompleter(un_applyDef: DefDef, companionContext: Context): TypeCompleter = + new CompleterWrapper(completerOf(un_applyDef)) { + override def complete(sym: Symbol): Unit = { + super.complete(sym) + + // If there's a same-named locked symbol, we're currently completing its signature. + // This means it (may) refer to us, and is thus either overloaded or recursive without a signature. + // rule out locked symbols from the owner.info.member call + val scopePartiallyCompleted = + companionContext.scope.lookupAll(sym.name).exists(existing => existing != sym && existing.hasFlag(LOCKED)) + + val suppress = + scopePartiallyCompleted || { + val userDefined = companionContext.owner.info.member(sym.name).filter(_ != sym) + (userDefined != NoSymbol) && { + userDefined.info match { + // TODO: do we have something for this already? the synthetic symbol can't be overloaded, right? + case OverloadedType(pre, alternatives) => + // pre probably relevant because of inherited overloads? + alternatives.exists(_.isErroneous) || alternatives.exists(alt => pre.memberInfo(alt) matches pre.memberInfo(sym)) + case tp => + (tp eq ErrorType) || tp.matches(sym.info) + } + } + } + + if (suppress) { + sym setInfo ErrorType + sym setFlag IS_ERROR + + // Don't unlink in an error situation to generate less confusing error messages. + // Ideally, our error reporting would distinguish overloaded from recursive user-defined apply methods without signature, + // but this would require some form of partial-completion of method signatures, so that we can + // know what the argument types were, even though we can't complete the result type, because + // we hit a cycle while trying to compute it (when we get here with locked user-defined symbols, we + // are in the complete for that symbol, and thus the locked symbol has not yet received enough info; + // I hesitate to provide more info, because it would involve a WildCard or something for its result type, + // which could upset other code paths) + if (!scopePartiallyCompleted) + companionContext.scope.unlink(sym) + } + } + } + def completerOf(tree: Tree): TypeCompleter = { val mono = namerOf(tree.symbol) monoTypeCompleter tree val tparams = treeInfo.typeParameters(tree) @@ -697,11 +748,17 @@ trait Namers extends MethodSynthesis { val bridgeFlag = if (mods hasAnnotationNamed tpnme.bridgeAnnot) BRIDGE | ARTIFACT else 0 val sym = assignAndEnterSymbol(tree) setFlag bridgeFlag - if (name == nme.copy && sym.isSynthetic) - enterCopyMethod(tree) - else - sym setInfo completerOf(tree) - } + // copy/apply/unapply synthetics are added using the addIfMissing mechanism, + // which ensures the owner has its preliminary info (we may add another decl here) + val completer = + if (sym hasFlag SYNTHETIC) { + if (name == nme.copy) copyMethodCompleter(tree) + else if (sym hasFlag CASE) applyUnapplyMethodCompleter(tree, context) + else completerOf(tree) + } else completerOf(tree) + + sym setInfo completer + } def enterClassDef(tree: ClassDef) { val ClassDef(mods, _, _, impl) = tree @@ -1351,7 +1408,7 @@ trait Namers extends MethodSynthesis { val defTpt = // don't mess with tpt's of case copy default getters, because assigning something other than TypeTree() - // will break the carefully orchestrated naming/typing logic that involves enterCopyMethod and caseClassCopyMeth + // will break the carefully orchestrated naming/typing logic that involves copyMethodCompleter and caseClassCopyMeth if (meth.isCaseCopy) TypeTree() else { // If the parameter type mentions any type parameter of the method, let the compiler infer the diff --git a/test/files/neg/userdefined_apply.check b/test/files/neg/userdefined_apply.check new file mode 100644 index 0000000000..ca0154885d --- /dev/null +++ b/test/files/neg/userdefined_apply.check @@ -0,0 +1,13 @@ +userdefined_apply.scala:3: error: overloaded method apply needs result type + private def apply(x: Int) = if (x > 0) new ClashOverloadNoSig(x) else apply("") + ^ +userdefined_apply.scala:12: error: overloaded method apply needs result type + private def apply(x: Int) = if (x > 0) ClashRecNoSig(1) else ??? + ^ +userdefined_apply.scala:19: error: overloaded method apply needs result type + private def apply(x: Boolean) = if (x) NoClashNoSig(1) else ??? + ^ +userdefined_apply.scala:26: error: overloaded method apply needs result type + private def apply(x: Boolean) = if (x) NoClashOverload(1) else apply("") + ^ +four errors found diff --git a/test/files/neg/userdefined_apply.scala b/test/files/neg/userdefined_apply.scala new file mode 100644 index 0000000000..1f2aff6e82 --- /dev/null +++ b/test/files/neg/userdefined_apply.scala @@ -0,0 +1,31 @@ +object ClashOverloadNoSig { + // error: overloaded method apply needs result type + private def apply(x: Int) = if (x > 0) new ClashOverloadNoSig(x) else apply("") + + def apply(x: String): ClashOverloadNoSig = ??? +} + +case class ClashOverloadNoSig private(x: Int) + +object ClashRecNoSig { + // error: recursive method apply needs result type + private def apply(x: Int) = if (x > 0) ClashRecNoSig(1) else ??? +} + +case class ClashRecNoSig private(x: Int) + +object NoClashNoSig { + // error: overloaded method apply needs result type + private def apply(x: Boolean) = if (x) NoClashNoSig(1) else ??? +} + +case class NoClashNoSig private(x: Int) + +object NoClashOverload { + // error: overloaded method apply needs result type + private def apply(x: Boolean) = if (x) NoClashOverload(1) else apply("") + + def apply(x: String): NoClashOverload = ??? +} + +case class NoClashOverload private(x: Int) diff --git a/test/files/pos/userdefined_apply.scala b/test/files/pos/userdefined_apply.scala new file mode 100644 index 0000000000..ca563f1dc5 --- /dev/null +++ b/test/files/pos/userdefined_apply.scala @@ -0,0 +1,36 @@ +// NOTE: the companion inherits a public apply method from Function1! +case class NeedsCompanion private (x: Int) + +object ClashNoSig { // ok + private def apply(x: Int) = if (x > 0) new ClashNoSig(x) else ??? +} +case class ClashNoSig private (x: Int) + + +object Clash { + private def apply(x: Int) = if (x > 0) new Clash(x) else ??? +} +case class Clash private (x: Int) + +object ClashSig { + private def apply(x: Int): ClashSig = if (x > 0) new ClashSig(x) else ??? +} +case class ClashSig private (x: Int) + +object ClashOverload { + private def apply(x: Int): ClashOverload = if (x > 0) new ClashOverload(x) else apply("") + def apply(x: String): ClashOverload = ??? +} +case class ClashOverload private (x: Int) + +object NoClashSig { + private def apply(x: Boolean): NoClashSig = if (x) NoClashSig(1) else ??? +} +case class NoClashSig private (x: Int) + +object NoClashOverload { + // needs full sig + private def apply(x: Boolean): NoClashOverload = if (x) NoClashOverload(1) else apply("") + def apply(x: String): NoClashOverload = ??? +} +case class NoClashOverload private (x: Int) -- cgit v1.2.3 From 575a668a38014f25cb4930ef2b1991a861ba558a Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Tue, 28 Feb 2017 10:24:45 -0800 Subject: Clarify spec of interaction of existing vs synthetic apply/unapply When matching user-defined apply/unapply members exist in a case class's companion object, don't add clashing synthetic ones. --- spec/05-classes-and-objects.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/05-classes-and-objects.md b/spec/05-classes-and-objects.md index 69828ec7fe..82f399fe47 100644 --- a/spec/05-classes-and-objects.md +++ b/spec/05-classes-and-objects.md @@ -851,9 +851,8 @@ already a `val` or `var` modifier. Hence, an accessor definition for the parameter is [generated](#class-definitions). A case class definition of `$c$[$\mathit{tps}\,$]($\mathit{ps}_1\,$)$\ldots$($\mathit{ps}_n$)` with type -parameters $\mathit{tps}$ and value parameters $\mathit{ps}$ implicitly -generates an [extractor object](08-pattern-matching.html#extractor-patterns) which is -defined as follows: +parameters $\mathit{tps}$ and value parameters $\mathit{ps}$ implies +the definition of a companion object, which serves as an [extractor object](08-pattern-matching.html#extractor-patterns). It has the following shape: ```scala object $c$ { @@ -870,11 +869,14 @@ each $\mathit{xs}\_i$ denotes the parameter names of the parameter section $\mathit{ps}\_i$, and $\mathit{xs}\_{11}, \ldots , \mathit{xs}\_{1k}$ denote the names of all parameters in the first parameter section $\mathit{xs}\_1$. -If a type parameter section is missing in the -class, it is also missing in the `apply` and -`unapply` methods. -The definition of `apply` is omitted if class $c$ is -`abstract`. +If a type parameter section is missing in the class, it is also missing in the `apply` and `unapply` methods. + +If the companion object $c$ is already defined, +the `apply` and `unapply` methods are added to the existing object. +The definition of `apply` is omitted if class $c$ is `abstract`. +If the object $c$ already defines a [matching](#definition-matching) member of the +same name as the synthetic member to be added, the synthetic member +is not added (overloading or mutual recursion is allowed, however). If the case class definition contains an empty value parameter list, the `unapply` method returns a `Boolean` instead of an `Option` type and @@ -887,9 +889,6 @@ def unapply[$\mathit{tps}\,$]($x$: $c$[$\mathit{tps}\,$]) = x ne null The name of the `unapply` method is changed to `unapplySeq` if the first parameter section $\mathit{ps}_1$ of $c$ ends in a [repeated parameter](04-basic-declarations-and-definitions.html#repeated-parameters). -If a companion object $c$ exists already, no new object is created, -but the `apply` and `unapply` methods are added to the existing -object instead. A method named `copy` is implicitly added to every case class unless the class already has a member (directly defined or inherited) with that name, or the -- cgit v1.2.3 From 615849058b5452b9d54ac152a1380ca7f81998c9 Mon Sep 17 00:00:00 2001 From: Adriaan Moors Date: Tue, 28 Feb 2017 14:14:11 -0800 Subject: Improvements based on reviews by Lukas & Jason --- spec/05-classes-and-objects.md | 5 +-- .../scala/tools/nsc/typechecker/Namers.scala | 48 ++++++++++++++-------- .../scala/reflect/internal/tpe/FindMembers.scala | 14 +++++++ test/files/neg/userdefined_apply.check | 20 +++++++-- test/files/neg/userdefined_apply.scala | 26 ++++++++++++ test/files/pos/userdefined_apply.scala | 18 ++++++++ 6 files changed, 107 insertions(+), 24 deletions(-) diff --git a/spec/05-classes-and-objects.md b/spec/05-classes-and-objects.md index 82f399fe47..65666e31cb 100644 --- a/spec/05-classes-and-objects.md +++ b/spec/05-classes-and-objects.md @@ -873,10 +873,9 @@ If a type parameter section is missing in the class, it is also missing in the ` If the companion object $c$ is already defined, the `apply` and `unapply` methods are added to the existing object. +If the object $c$ already has a [matching](#definition-matching) +`apply` (or `unapply`) member, no new definition is added. The definition of `apply` is omitted if class $c$ is `abstract`. -If the object $c$ already defines a [matching](#definition-matching) member of the -same name as the synthetic member to be added, the synthetic member -is not added (overloading or mutual recursion is allowed, however). If the case class definition contains an empty value parameter list, the `unapply` method returns a `Boolean` instead of an `Option` type and diff --git a/src/compiler/scala/tools/nsc/typechecker/Namers.scala b/src/compiler/scala/tools/nsc/typechecker/Namers.scala index 19607b6681..69b8cb12e6 100644 --- a/src/compiler/scala/tools/nsc/typechecker/Namers.scala +++ b/src/compiler/scala/tools/nsc/typechecker/Namers.scala @@ -647,25 +647,41 @@ trait Namers extends MethodSynthesis { def applyUnapplyMethodCompleter(un_applyDef: DefDef, companionContext: Context): TypeCompleter = new CompleterWrapper(completerOf(un_applyDef)) { override def complete(sym: Symbol): Unit = { + assert(sym hasAllFlags CASE | SYNTHETIC, sym.defString) + super.complete(sym) + // owner won't be locked + val ownerInfo = companionContext.owner.info + // If there's a same-named locked symbol, we're currently completing its signature. - // This means it (may) refer to us, and is thus either overloaded or recursive without a signature. - // rule out locked symbols from the owner.info.member call - val scopePartiallyCompleted = - companionContext.scope.lookupAll(sym.name).exists(existing => existing != sym && existing.hasFlag(LOCKED)) - - val suppress = - scopePartiallyCompleted || { - val userDefined = companionContext.owner.info.member(sym.name).filter(_ != sym) + // If `scopePartiallyCompleted`, the program is known to have a type error, since + // this means a user-defined method is missing a result type while its rhs refers to `sym` or an overload. + // This is an error because overloaded/recursive methods must have a result type. + // The method would be overloaded if its signature, once completed, would not match the synthetic method's, + // or recursive if it turned out we should unlink our synthetic method (matching sig). + // In any case, error out. We don't unlink the symbol so that `symWasOverloaded` says yes, + // which would be wrong if the method is in fact recursive, but it seems less confusing. + val scopePartiallyCompleted = new HasMember(ownerInfo, sym.name, BridgeFlags | SYNTHETIC, LOCKED).apply() + + // Check `scopePartiallyCompleted` first to rule out locked symbols from the owner.info.member call, + // as FindMember will call info on a locked symbol (while checking type matching to assemble an overloaded type), + // and throw a TypeError, so that we are aborted. + // Do not consider deferred symbols, as suppressing our concrete implementation would be an error regardless + // of whether the signature matches (if it matches, we omitted a valid implementation, if it doesn't, + // we would get an error for the missing implementation it isn't implemented by some overload other than our synthetic one) + val suppress = scopePartiallyCompleted || { + // can't exclude deferred members using DEFERRED flag here (TODO: why?) + val userDefined = ownerInfo.memberBasedOnName(sym.name, BridgeFlags | SYNTHETIC) + (userDefined != NoSymbol) && { - userDefined.info match { - // TODO: do we have something for this already? the synthetic symbol can't be overloaded, right? - case OverloadedType(pre, alternatives) => - // pre probably relevant because of inherited overloads? - alternatives.exists(_.isErroneous) || alternatives.exists(alt => pre.memberInfo(alt) matches pre.memberInfo(sym)) - case tp => - (tp eq ErrorType) || tp.matches(sym.info) + assert(userDefined != sym) + val alts = userDefined.alternatives // could be just the one, if this member isn't overloaded + // don't compute any further `memberInfo`s if there's an error somewhere + alts.exists(_.isErroneous) || { + val self = companionContext.owner.thisType + val memberInfo = self.memberInfo(sym) + alts.exists(alt => !alt.isDeferred && (self.memberInfo(alt) matches memberInfo)) } } } @@ -748,8 +764,6 @@ trait Namers extends MethodSynthesis { val bridgeFlag = if (mods hasAnnotationNamed tpnme.bridgeAnnot) BRIDGE | ARTIFACT else 0 val sym = assignAndEnterSymbol(tree) setFlag bridgeFlag - // copy/apply/unapply synthetics are added using the addIfMissing mechanism, - // which ensures the owner has its preliminary info (we may add another decl here) val completer = if (sym hasFlag SYNTHETIC) { if (name == nme.copy) copyMethodCompleter(tree) diff --git a/src/reflect/scala/reflect/internal/tpe/FindMembers.scala b/src/reflect/scala/reflect/internal/tpe/FindMembers.scala index 83a5d23e7c..1b00815bca 100644 --- a/src/reflect/scala/reflect/internal/tpe/FindMembers.scala +++ b/src/reflect/scala/reflect/internal/tpe/FindMembers.scala @@ -285,4 +285,18 @@ trait FindMembers { initBaseClasses.head.newOverloaded(tpe, members) } } + + private[scala] final class HasMember(tpe: Type, name: Name, excludedFlags: Long, requiredFlags: Long) extends FindMemberBase[Boolean](tpe, name, excludedFlags, requiredFlags) { + private[this] var _result = false + override protected def result: Boolean = _result + + protected def shortCircuit(sym: Symbol): Boolean = { + _result = true + true // prevents call to addMemberIfNew + } + + // Not used + protected def addMemberIfNew(sym: Symbol): Unit = {} + } + } diff --git a/test/files/neg/userdefined_apply.check b/test/files/neg/userdefined_apply.check index ca0154885d..c8c8976f5f 100644 --- a/test/files/neg/userdefined_apply.check +++ b/test/files/neg/userdefined_apply.check @@ -1,13 +1,25 @@ userdefined_apply.scala:3: error: overloaded method apply needs result type private def apply(x: Int) = if (x > 0) new ClashOverloadNoSig(x) else apply("") ^ -userdefined_apply.scala:12: error: overloaded method apply needs result type +userdefined_apply.scala:14: error: overloaded method apply needs result type private def apply(x: Int) = if (x > 0) ClashRecNoSig(1) else ??? ^ -userdefined_apply.scala:19: error: overloaded method apply needs result type +userdefined_apply.scala:21: error: overloaded method apply needs result type private def apply(x: Boolean) = if (x) NoClashNoSig(1) else ??? ^ -userdefined_apply.scala:26: error: overloaded method apply needs result type +userdefined_apply.scala:28: error: overloaded method apply needs result type private def apply(x: Boolean) = if (x) NoClashOverload(1) else apply("") ^ -four errors found +userdefined_apply.scala:45: error: recursive method apply needs result type +case class NoClashNoSigPoly private(x: Int) + ^ +userdefined_apply.scala:39: error: NoClashNoSigPoly.type does not take parameters + def apply(x: T) = if (???) NoClashNoSigPoly(1) else ??? + ^ +userdefined_apply.scala:57: error: recursive method apply needs result type +case class ClashNoSigPoly private(x: Int) + ^ +userdefined_apply.scala:51: error: ClashNoSigPoly.type does not take parameters + def apply(x: T) = if (???) ClashNoSigPoly(1) else ??? + ^ +8 errors found diff --git a/test/files/neg/userdefined_apply.scala b/test/files/neg/userdefined_apply.scala index 1f2aff6e82..0a0d960b39 100644 --- a/test/files/neg/userdefined_apply.scala +++ b/test/files/neg/userdefined_apply.scala @@ -8,6 +8,8 @@ object ClashOverloadNoSig { case class ClashOverloadNoSig private(x: Int) object ClashRecNoSig { + // TODO: status quo is that the error refers to an overloaded method, which is actually recursive + // (we should have unlinked the symbol in the `if(suppress)` part of `applyUnapplyMethodCompleter`) // error: recursive method apply needs result type private def apply(x: Int) = if (x > 0) ClashRecNoSig(1) else ??? } @@ -29,3 +31,27 @@ object NoClashOverload { } case class NoClashOverload private(x: Int) + + +class BaseNCNSP[T] { + // TODO: suppress the following error + // error: NoClashNoSigPoly.type does not take parameters + def apply(x: T) = if (???) NoClashNoSigPoly(1) else ??? +} + +object NoClashNoSigPoly extends BaseNCNSP[Boolean] +// TODO: position error at definition of apply in superclass instead of on case clss +// error: recursive method apply needs result type +case class NoClashNoSigPoly private(x: Int) + + +class BaseCNSP[T] { + // TODO: suppress the following error + // error: ClashNoSigPoly.type does not take parameters + def apply(x: T) = if (???) ClashNoSigPoly(1) else ??? +} + +object ClashNoSigPoly extends BaseCNSP[Int] +// TODO: position error at definition of apply in superclass instead of on case clss +// error: recursive method apply needs result type +case class ClashNoSigPoly private(x: Int) diff --git a/test/files/pos/userdefined_apply.scala b/test/files/pos/userdefined_apply.scala index ca563f1dc5..e29f9f5141 100644 --- a/test/files/pos/userdefined_apply.scala +++ b/test/files/pos/userdefined_apply.scala @@ -34,3 +34,21 @@ object NoClashOverload { def apply(x: String): NoClashOverload = ??? } case class NoClashOverload private (x: Int) + + + +class BaseNCP[T] { + // error: overloaded method apply needs result type + def apply(x: T): NoClashPoly = if (???) NoClashPoly(1) else ??? +} + +object NoClashPoly extends BaseNCP[Boolean] +case class NoClashPoly private(x: Int) + + +class BaseCP[T] { + // error: overloaded method apply needs result type + def apply(x: T): ClashPoly = if (???) ClashPoly(1) else ??? +} +object ClashPoly extends BaseCP[Int] +case class ClashPoly private(x: Int) -- cgit v1.2.3