From f0c1894ef1d677d3b7f42a83a1e2c2ae84a0e3ba Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 10:01:25 +0100 Subject: [PATCH 01/13] Take implicit methods into account when computing candidates for extension methods --- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index df053e84d91e..30267e3d3cde 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -93,7 +93,9 @@ object Implicits { def viewCandidateKind(tpw: Type, argType: Type, resType: Type): Candidate.Kind = { def methodCandidateKind(mt: MethodType, approx: Boolean) = - if (!mt.isImplicitMethod && + if (mt.isImplicitMethod) + viewCandidateKind(normalize(mt, pt), argType, resType) + else if (!mt.isImplicitMethod && mt.paramInfos.lengthCompare(1) == 0 && { var formal = widenSingleton(mt.paramInfos.head) if (approx) formal = wildApprox(formal) From f5b6463f4187f06e2e7c9daf180a7cc621c88f22 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 10:05:01 +0100 Subject: [PATCH 02/13] Pick most significant error in implicit search In implicit search, if both contextual and type search fail, pick the more significant error of the two searches to report. - ambiguous trumps other errors - otherwise, errors with larger computed results trump errors with smaller ones. --- .../src/dotty/tools/dotc/typer/Implicits.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 30267e3d3cde..ffd36af6feeb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -95,8 +95,7 @@ object Implicits { def methodCandidateKind(mt: MethodType, approx: Boolean) = if (mt.isImplicitMethod) viewCandidateKind(normalize(mt, pt), argType, resType) - else if (!mt.isImplicitMethod && - mt.paramInfos.lengthCompare(1) == 0 && { + else if (mt.paramInfos.lengthCompare(1) == 0 && { var formal = widenSingleton(mt.paramInfos.head) if (approx) formal = wildApprox(formal) ctx.test(implicit ctx => argType relaxed_<:< formal) @@ -1276,9 +1275,13 @@ trait Implicits { self: Typer => case reason => if (contextual) bestImplicit(contextual = false).recoverWith { - failure2 => reason match { - case (_: DivergingImplicit) | (_: ShadowedImplicit) => failure - case _ => failure2 + failure2 => failure2.reason match { + case _: AmbiguousImplicits => failure2 + case _ => + reason match { + case (_: DivergingImplicit) | (_: ShadowedImplicit) => failure + case _ => List(failure, failure2).maxBy(_.tree.treeSize) + } } } else failure @@ -1637,4 +1640,6 @@ final class TermRefSet(implicit ctx: Context) { foreach(tr => buffer += tr) buffer.toList } + + override def toString = toList.toString } From 03e0900bb8faa5c09ff012e4bd9f7358574853f9 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 10:14:55 +0100 Subject: [PATCH 03/13] Report failed extension method constructions as addendums to error messages --- .../tools/dotc/printing/PlainPrinter.scala | 2 +- .../dotc/reporting/diagnostic/messages.scala | 4 +- .../dotty/tools/dotc/typer/TypeAssigner.scala | 54 +++++++++++-------- .../src/dotty/tools/dotc/typer/Typer.scala | 13 +++-- tests/neg/i5773.scala | 32 +++++++++++ 5 files changed, 76 insertions(+), 29 deletions(-) create mode 100644 tests/neg/i5773.scala diff --git a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala index e45ab1c4a486..c8a13900b801 100644 --- a/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala +++ b/compiler/src/dotty/tools/dotc/printing/PlainPrinter.scala @@ -524,7 +524,7 @@ class PlainPrinter(_ctx: Context) extends Printer { case result: AmbiguousImplicits => "Ambiguous Implicit: " ~ toText(result.alt1.ref) ~ " and " ~ toText(result.alt2.ref) case _ => - "?Unknown Implicit Result?" + result.getClass + "Search Failure: " ~ toText(result.tree) } } diff --git a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala index 6cbc0526b9d6..5961935e02b0 100644 --- a/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala +++ b/compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala @@ -300,7 +300,7 @@ object messages { val explanation: String = "" } - case class NotAMember(site: Type, name: Name, selected: String)(implicit ctx: Context) + case class NotAMember(site: Type, name: Name, selected: String, addendum: String = "")(implicit ctx: Context) extends Message(NotAMemberID) { val kind: String = "Member Not Found" @@ -360,7 +360,7 @@ object messages { ) } - ex"$selected $name is not a member of ${site.widen}$closeMember" + ex"$selected $name is not a member of ${site.widen}$closeMember$addendum" } val explanation: String = "" diff --git a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala index d66fe8eeb16d..c04066ad24b1 100644 --- a/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala +++ b/compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala @@ -234,38 +234,46 @@ trait TypeAssigner { test(tpe, true) } - /** The type of a selection with `name` of a tree with type `site`. - */ - def selectionType(site: Type, name: Name, pos: SourcePosition)(implicit ctx: Context): Type = { - val mbr = site.member(name) + /** The type of the selection `tree`, where `qual1` is the typed qualifier part. */ + def selectionType(tree: untpd.RefTree, qual1: Tree)(implicit ctx: Context): Type = { + var qualType = qual1.tpe.widenIfUnstable + if (!qualType.hasSimpleKind && tree.name != nme.CONSTRUCTOR) + // constructors are selected on typeconstructor, type arguments are passed afterwards + qualType = errorType(em"$qualType takes type parameters", qual1.sourcePos) + else if (!qualType.isInstanceOf[TermType]) + qualType = errorType(em"$qualType is illegal as a selection prefix", qual1.sourcePos) + val name = tree.name + val mbr = qualType.member(name) if (reallyExists(mbr)) - site.select(name, mbr) - else if (site.derivesFrom(defn.DynamicClass) && !Dynamic.isDynamicMethod(name)) + qualType.select(name, mbr) + else if (qualType.derivesFrom(defn.DynamicClass) && !Dynamic.isDynamicMethod(name)) TryDynamicCallType - else if (site.isErroneous || name.toTermName == nme.ERROR) + else if (qualType.isErroneous || name.toTermName == nme.ERROR) UnspecifiedErrorType + else if (name == nme.CONSTRUCTOR) + errorType(ex"$qualType does not have a constructor", tree.sourcePos) else { - def kind = if (name.isTypeName) "type" else "value" - def addendum = - if (site.derivesFrom(defn.DynamicClass)) "\npossible cause: maybe a wrong Dynamic method signature?" - else "" - errorType( - if (name == nme.CONSTRUCTOR) ex"$site does not have a constructor" - else NotAMember(site, name, kind), - pos) + val kind = if (name.isTypeName) "type" else "value" + val addendum = + if (qualType.derivesFrom(defn.DynamicClass)) + "\npossible cause: maybe a wrong Dynamic method signature?" + else qual1.getAttachment(Typer.HiddenSearchFailure) match { + case Some(failure) if !failure.reason.isInstanceOf[Implicits.NoMatchingImplicits] => + i""". + |An extension method was tried, but could not be fully constructed: + | + | ${failure.tree.show.replace("\n", "\n ")}""" + case _ => "" + } + errorType(NotAMember(qualType, name, kind, addendum), tree.sourcePos) } } - /** The selection type, which is additionally checked for accessibility. + /** The type of the selection in `tree`, where `qual1` is the typed qualifier part. + * The selection type is additionally checked for accessibility. */ def accessibleSelectionType(tree: untpd.RefTree, qual1: Tree)(implicit ctx: Context): Type = { - var qualType = qual1.tpe.widenIfUnstable - if (!qualType.hasSimpleKind && tree.name != nme.CONSTRUCTOR) - // constructors are selected on typeconstructor, type arguments are passed afterwards - qualType = errorType(em"$qualType takes type parameters", qual1.sourcePos) - else if (!qualType.isInstanceOf[TermType]) - qualType = errorType(em"$qualType is illegal as a selection prefix", qual1.sourcePos) - val ownType = selectionType(qualType, tree.name, tree.sourcePos) + val ownType = selectionType(tree, qual1) if (tree.getAttachment(desugar.SuppressAccessCheck).isDefined) ownType else ensureAccessible(ownType, qual1.isInstanceOf[Super], tree.sourcePos) } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index c792f939aaf4..7b15d1080159 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -40,7 +40,6 @@ import transform.SymUtils._ import transform.TypeUtils._ import reporting.trace - object Typer { /** The precedence of bindings which determines which of several bindings will be @@ -75,6 +74,11 @@ object Typer { * the `()` was dropped by the Typer. */ private val DroppedEmptyArgs = new Property.Key[Unit] + + /** Am attachment that indicates a failed conversion or extension method + * search was tried on a tree. This will in some cases be reported in error messages + */ + private[typer] val HiddenSearchFailure = new Property.Key[SearchFailure] } class Typer extends Namer @@ -2724,11 +2728,14 @@ class Typer extends Namer checkImplicitConversionUseOK(inferred.symbol, tree.posd) readapt(inferred)(ctx.retractMode(Mode.ImplicitsEnabled)) case failure: SearchFailure => - if (pt.isInstanceOf[ProtoType] && !failure.isAmbiguous) + if (pt.isInstanceOf[ProtoType] && !failure.isAmbiguous) { // don't report the failure but return the tree unchanged. This // will cause a failure at the next level out, which usually gives - // a better error message. + // a better error message. To compensate, store the encountered failure + // as an attachment, so that it can be reported later as an addendum. + tree.putAttachment(HiddenSearchFailure, failure) tree + } else recover(failure.reason) } else recover(NoMatchingImplicits) diff --git a/tests/neg/i5773.scala b/tests/neg/i5773.scala new file mode 100644 index 000000000000..de19a7449fab --- /dev/null +++ b/tests/neg/i5773.scala @@ -0,0 +1,32 @@ +trait Semigroup[T] { + def (lhs: T) append (rhs: T): T +} + +object Semigroup { + implicit object stringAppend extends Semigroup[String] { + override def (lhs: String) append (rhs: String): String = lhs + rhs + } + + implicit def sumSemigroup[N](implicit N: Numeric[N]): Semigroup[N] = new { + override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) + } + + implicit class SumSemigroupDeco[N](implicit N: Numeric[N]) extends Semigroup[N] { + override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) + } +} + + +object Main { + import Semigroup.sumSemigroup // this is not sufficient + def f1 = { + import Semigroup.stringAppend // necessary to make the extension method visible + println("Hi" append " mum") // ok + println(1 append 2) // error: this won't compile + } + + def f2 = { + implicit val intSumAppend: Semigroup[Int] = sumSemigroup[Int] + println(3 append 4) // this will + } +} \ No newline at end of file From 6d50a34bff23b31d6736cbf0107ff70c303fcf1b Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 16:15:00 +0100 Subject: [PATCH 04/13] Keep constraints for subtype tests with ProtoTypes A subtype test `A <:< P` where `P` is a prototype was implemented as `P.isMatchedBy(A)`. `isMatchedBy` did not keep the constraint which meant that information was lost instead of being propagated. --- .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- .../src/dotty/tools/dotc/core/Types.scala | 2 +- .../dotty/tools/dotc/typer/Applications.scala | 22 ++++++------- .../dotty/tools/dotc/typer/ProtoTypes.scala | 32 +++++++++++++------ 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 240d2988e473..5d2367a633b5 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -1227,7 +1227,7 @@ class TypeComparer(initctx: Context) extends ConstraintHandling[AbsentContext] { /** Defer constraining type variables when compared against prototypes */ def isMatchedByProto(proto: ProtoType, tp: Type): Boolean = tp.stripTypeVar match { case tp: TypeParamRef if constraint contains tp => true - case _ => proto.isMatchedBy(tp) + case _ => proto.isMatchedBy(tp, keepConstraint = true) } /** Narrow gadt.bounds for the type parameter referenced by `tr` to include diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index e246bc74f358..3e7f7669b3a2 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1665,7 +1665,7 @@ object Types { /** A trait for proto-types, used as expected types in typer */ trait ProtoType extends Type { - def isMatchedBy(tp: Type)(implicit ctx: Context): Boolean + def isMatchedBy(tp: Type, keepConstraint: Boolean = false)(implicit ctx: Context): Boolean def fold[T](x: T, ta: TypeAccumulator[T])(implicit ctx: Context): T def map(tm: TypeMap)(implicit ctx: Context): ProtoType diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 0a3af9b047f0..822d05481f90 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -277,10 +277,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => private[this] var _ok = true def ok: Boolean = _ok - def ok_=(x: Boolean): Unit = { - assert(x || ctx.reporter.errorsReported || !ctx.typerState.isCommittable) // !!! DEBUG - _ok = x - } + def ok_=(x: Boolean): Unit = _ok = x /** The function's type after widening and instantiating polytypes * with TypeParamRefs in constraint set @@ -1118,8 +1115,11 @@ trait Applications extends Compatibility { self: Typer with Dynamic => /** Is given method reference applicable to type arguments `targs` and argument trees `args`? * @param resultType The expected result type of the application */ - def isApplicable(methRef: TermRef, targs: List[Type], args: List[Tree], resultType: Type)(implicit ctx: Context): Boolean = - ctx.test(implicit ctx => new ApplicableToTrees(methRef, targs, args, resultType).success) + def isApplicable(methRef: TermRef, targs: List[Type], args: List[Tree], resultType: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { + def isApp(implicit ctx: Context): Boolean = + new ApplicableToTrees(methRef, targs, args, resultType).success + if (keepConstraint) isApp else ctx.test(implicit ctx => isApp) + } /** Is given method reference applicable to type arguments `targs` and argument trees `args` without inferring views? * @param resultType The expected result type of the application @@ -1137,8 +1137,8 @@ trait Applications extends Compatibility { self: Typer with Dynamic => * possibly after inserting an `apply`? * @param resultType The expected result type of the application */ - def isApplicable(tp: Type, targs: List[Type], args: List[Tree], resultType: Type)(implicit ctx: Context): Boolean = - onMethod(tp, isApplicable(_, targs, args, resultType)) + def isApplicable(tp: Type, targs: List[Type], args: List[Tree], resultType: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = + onMethod(tp, isApplicable(_, targs, args, resultType, keepConstraint)) /** Is given type applicable to argument types `args`, possibly after inserting an `apply`? * @param resultType The expected result type of the application @@ -1491,7 +1491,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => ) if (alts2.isEmpty && !ctx.isAfterTyper) alts.filter(alt => - isApplicable(alt, targs, args, resultType) + isApplicable(alt, targs, args, resultType, keepConstraint = false) ) else alts2 @@ -1511,14 +1511,14 @@ trait Applications extends Compatibility { self: Typer with Dynamic => } case pt @ PolyProto(targs1, pt1) if targs.isEmpty => - val alts1 = alts filter pt.isMatchedBy + val alts1 = alts.filter(pt.isMatchedBy(_)) resolveOverloaded(alts1, pt1, targs1.tpes) case defn.FunctionOf(args, resultType, _, _) => narrowByTypes(alts, args, resultType) case pt => - val compat = alts.filter(normalizedCompatible(_, pt)) + val compat = alts.filter(normalizedCompatible(_, pt, keepConstraint = false)) if (compat.isEmpty) /* * the case should not be moved to the enclosing match diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 27f677324e7e..bdc082943467 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -35,12 +35,21 @@ object ProtoTypes { def isCompatible(tp: Type, pt: Type)(implicit ctx: Context): Boolean = (tp.widenExpr relaxed_<:< pt.widenExpr) || viewExists(tp, pt) - /** Test compatibility after normalization in a fresh typerstate. */ - def normalizedCompatible(tp: Type, pt: Type)(implicit ctx: Context): Boolean = - ctx.test { implicit ctx => + /** Test compatibility after normalization. + * Do this in a fresh typerstate unless `keepConstraint` is true. + */ + def normalizedCompatible(tp: Type, pt: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { + def testCompat(implicit ctx: Context): Boolean = { val normTp = normalize(tp, pt) isCompatible(normTp, pt) || pt.isRef(defn.UnitClass) && normTp.isParameterless } + if (keepConstraint) + tp.widenSingleton match { + case poly: PolyType => normalizedCompatible(tp, pt, keepConstraint = false) + case _ => testCompat + } + else ctx.test(implicit ctx => testCompat) + } private def disregardProto(pt: Type)(implicit ctx: Context): Boolean = pt.dealias match { case _: OrType => true @@ -89,7 +98,7 @@ object ProtoTypes { /** A trait for prototypes that match all types */ trait MatchAlways extends ProtoType { - def isMatchedBy(tp1: Type)(implicit ctx: Context): Boolean = true + def isMatchedBy(tp1: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = true def map(tm: TypeMap)(implicit ctx: Context): ProtoType = this def fold[T](x: T, ta: TypeAccumulator[T])(implicit ctx: Context): T = x override def toString: String = getClass.toString @@ -131,13 +140,13 @@ object ProtoTypes { case _ => false } - override def isMatchedBy(tp1: Type)(implicit ctx: Context): Boolean = { + override def isMatchedBy(tp1: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { name == nme.WILDCARD || hasUnknownMembers(tp1) || { val mbr = if (privateOK) tp1.member(name) else tp1.nonPrivateMember(name) def qualifies(m: SingleDenotation) = memberProto.isRef(defn.UnitClass) || - tp1.isValueType && compat.normalizedCompatible(NamedType(tp1, name, m), memberProto) + tp1.isValueType && compat.normalizedCompatible(NamedType(tp1, name, m), memberProto, keepConstraint) // Note: can't use `m.info` here because if `m` is a method, `m.info` // loses knowledge about `m`'s default arguments. mbr match { // hasAltWith inlined for performance @@ -234,8 +243,11 @@ object ProtoTypes { extends UncachedGroundType with ApplyingProto with FunOrPolyProto { override def resultType(implicit ctx: Context): Type = resType - def isMatchedBy(tp: Type)(implicit ctx: Context): Boolean = - typer.isApplicable(tp, Nil, unforcedTypedArgs, resultType) + def isMatchedBy(tp: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { + val args = unforcedTypedArgs + def isPoly(tree: Tree) = tree.tpe.widenSingleton.isInstanceOf[PolyType] + typer.isApplicable(tp, Nil, args, resultType, keepConstraint && !args.exists(isPoly)) + } def derivedFunProto(args: List[untpd.Tree] = this.args, resultType: Type, typer: Typer = this.typer): FunProto = if ((args eq this.args) && (resultType eq this.resultType) && (typer eq this.typer)) this @@ -379,7 +391,7 @@ object ProtoTypes { override def resultType(implicit ctx: Context): Type = resType - def isMatchedBy(tp: Type)(implicit ctx: Context): Boolean = + def isMatchedBy(tp: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = ctx.typer.isApplicable(tp, argType :: Nil, resultType) || { resType match { case SelectionProto(name: TermName, mbrType, _, _) => @@ -422,7 +434,7 @@ object ProtoTypes { override def resultType(implicit ctx: Context): Type = resType - override def isMatchedBy(tp: Type)(implicit ctx: Context): Boolean = { + override def isMatchedBy(tp: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { def isInstantiatable(tp: Type) = tp.widen match { case tp: PolyType => tp.paramNames.length == targs.length case _ => false From 8e22afdcb5d058a429820ea597f1af78fcc0760f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 17:25:39 +0100 Subject: [PATCH 05/13] Don't cache unforced typedArgs Don't cache the result of an unforced typedArgs if it is a WildcardType. We might come back later and want the forced version. --- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index bdc082943467..69e1d95447b8 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -304,11 +304,13 @@ object ProtoTypes { * with unknown parameter types - this will then cause a * "missing parameter type" error */ - private def typedArgs(force: Boolean): List[Tree] = { - if (state.typedArgs.size != args.length) - state.typedArgs = args.mapconserve(cacheTypedArg(_, typer.typed(_), force)) - state.typedArgs - } + private def typedArgs(force: Boolean): List[Tree] = + if (state.typedArgs.size == args.length) state.typedArgs + else { + val args1 = args.mapconserve(cacheTypedArg(_, typer.typed(_), force)) + if (!args1.contains(WildcardType)) state.typedArgs = args1 + args1 + } def typedArgs: List[Tree] = typedArgs(force = true) def unforcedTypedArgs: List[Tree] = typedArgs(force = false) From 6800e09cd0bee5546709f66ff32310764d48f154 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 19:24:07 +0100 Subject: [PATCH 06/13] Drop all IgnoredProto parts before implicit search Drop all IgnoredProto parts of the expected type before inferring the arguments of an implicit method. That way, we propagate all available info by `constrainResult` into the current constraint before doing the implicit search. This prevents ambiguity errors. See pos/i5773.scala. --- compiler/src/dotty/tools/dotc/core/Mode.scala | 4 +-- .../dotty/tools/dotc/typer/Applications.scala | 6 ++-- .../src/dotty/tools/dotc/typer/Typer.scala | 14 ++++++-- tests/neg/i5773.scala | 26 +++++++-------- tests/pos/i5773.scala | 33 +++++++++++++++++++ 5 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 tests/pos/i5773.scala diff --git a/compiler/src/dotty/tools/dotc/core/Mode.scala b/compiler/src/dotty/tools/dotc/core/Mode.scala index bef2a0e50c2b..c23fbb4f4ad8 100644 --- a/compiler/src/dotty/tools/dotc/core/Mode.scala +++ b/compiler/src/dotty/tools/dotc/core/Mode.scala @@ -104,6 +104,6 @@ object Mode { /** Read comments from definitions when unpickling from TASTY */ val ReadComments: Mode = newMode(22, "ReadComments") - /** Suppress insertion of apply or implicit conversion on qualifier */ - val FixedQualifier: Mode = newMode(23, "FixedQualifier") + /** We are synthesizing the receiver of an extension method */ + val SynthesizeExtMethodReceiver: Mode = newMode(23, "SynthesizeExtMethodReceiver") } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 822d05481f90..92e5dd820b0e 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -799,7 +799,9 @@ trait Applications extends Compatibility { self: Typer with Dynamic => * part. Return an optional value to indicate success. */ def tryWithImplicitOnQualifier(fun1: Tree, proto: FunProto)(implicit ctx: Context): Option[Tree] = - if (ctx.mode.is(Mode.FixedQualifier)) None + if (ctx.mode.is(Mode.SynthesizeExtMethodReceiver)) + // Suppress insertion of apply or implicit conversion on extension method receiver + None else tryInsertImplicitOnQualifier(fun1, proto, ctx.typerState.ownedVars) flatMap { fun2 => tryEither { @@ -1691,7 +1693,7 @@ trait Applications extends Compatibility { self: Typer with Dynamic => } val app = typed(untpd.Apply(core, untpd.TypedSplice(receiver) :: Nil), pt1, ctx.typerState.ownedVars)( - ctx.addMode(Mode.FixedQualifier)) + ctx.addMode(Mode.SynthesizeExtMethodReceiver)) if (!app.symbol.is(Extension)) ctx.error(em"not an extension method: $methodRef", receiver.sourcePos) app diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 7b15d1080159..febf73d7818c 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2211,7 +2211,9 @@ class Typer extends Namer def tryImplicit(fallBack: => Tree) = tryInsertImplicitOnQualifier(tree, pt.withContext(ctx), locked).getOrElse(fallBack) - if (ctx.mode.is(Mode.FixedQualifier)) tree + if (ctx.mode.is(Mode.SynthesizeExtMethodReceiver)) + // Suppress insertion of apply or implicit conversion on extension method receiver + tree else pt match { case pt @ FunProto(Nil, _) if tree.symbol.allOverriddenSymbols.exists(_.info.isNullaryMethod) && @@ -2395,6 +2397,13 @@ class Typer extends Namer } } + /** Reveal ignored parts of prototype when synthesizing the receiver + * of an extension method. This is necessary for pos/i5773.scala + */ + def revealProtoOfExtMethod(tp: Type)(implicit ctx: Context): Type = + if (ctx.mode.is(Mode.SynthesizeExtMethodReceiver)) tp.deepenProto + else tp + def adaptNoArgsImplicitMethod(wtp: MethodType): Tree = { assert(wtp.isImplicitMethod) val tvarsToInstantiate = tvarsInParams(tree, locked).distinct @@ -2606,7 +2615,8 @@ class Typer extends Namer def adaptNoArgs(wtp: Type): Tree = { val ptNorm = underlyingApplied(pt) def functionExpected = defn.isFunctionType(ptNorm) - def resultMatch = constrainResult(tree.symbol, wtp, followAlias(pt)) + def resultMatch = + constrainResult(tree.symbol, wtp, revealProtoOfExtMethod(followAlias(pt))) def needsEta = pt match { case _: SingletonType => false case IgnoredProto(_: FunOrPolyProto) => false diff --git a/tests/neg/i5773.scala b/tests/neg/i5773.scala index de19a7449fab..6dae20eb3e45 100644 --- a/tests/neg/i5773.scala +++ b/tests/neg/i5773.scala @@ -1,5 +1,6 @@ trait Semigroup[T] { def (lhs: T) append (rhs: T): T + def (lhs: Int) appendS (rhs: T): T = ??? } object Semigroup { @@ -8,11 +9,8 @@ object Semigroup { } implicit def sumSemigroup[N](implicit N: Numeric[N]): Semigroup[N] = new { - override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) - } - - implicit class SumSemigroupDeco[N](implicit N: Numeric[N]) extends Semigroup[N] { - override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) + override def (lhs: N) append (rhs: N): N = N.plus(lhs, rhs) + def (lhs: Int) appendS (rhs: N): N = ??? // N.plus(lhs, rhs) } } @@ -20,13 +18,15 @@ object Semigroup { object Main { import Semigroup.sumSemigroup // this is not sufficient def f1 = { - import Semigroup.stringAppend // necessary to make the extension method visible - println("Hi" append " mum") // ok - println(1 append 2) // error: this won't compile - } - - def f2 = { - implicit val intSumAppend: Semigroup[Int] = sumSemigroup[Int] - println(3 append 4) // this will + println(1 appendS 2) // error This should give the following error message: +/* +21 | println(1 appendS 2) // error This should give the following error message: + | ^^^^^^^^^ + |value appendS is not a member of Int. + |An extension method was tried, but could not be fully constructed: + | + | Semigroup.sumSemigroup[Any](/* ambiguous */implicitly[Numeric[Any]]).appendS() +one error found +*/ } } \ No newline at end of file diff --git a/tests/pos/i5773.scala b/tests/pos/i5773.scala new file mode 100644 index 000000000000..70fee71ec15f --- /dev/null +++ b/tests/pos/i5773.scala @@ -0,0 +1,33 @@ +trait Semigroup[T] { + def (lhs: T) append (rhs: T): T +} + +object Semigroup { + implicit object stringAppend extends Semigroup[String] { + override def (lhs: String) append (rhs: String): String = lhs + rhs + } + + implicit def sumSemigroup[N](implicit N: Numeric[N]): Semigroup[N] = new { + override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) + } + + implicit class SumSemigroupDeco[N](implicit N: Numeric[N]) extends Semigroup[N] { + override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) + } +} + + +object Main { + import Semigroup.sumSemigroup // this is not sufficient + def f1 = { + import Semigroup.stringAppend // necessary to make the extension method visible + println("Hi" append " mum") // ok + //sumSemigroup.apply(1)(2) + println(1 append 2) // error: this won't compile + } + + def f2 = { + implicit val intSumAppend: Semigroup[Int] = sumSemigroup[Int] + println(3 append 4) // this will + } +} \ No newline at end of file From 7d7e35e944b899f0a665011f1ed50df86cecbf55 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 19:24:58 +0100 Subject: [PATCH 07/13] Recursively drop IgnoredProto parts When doping a deepenProto, strip all levels of IgnoredProto, not just one. --- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 69e1d95447b8..0451384b2270 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -107,7 +107,7 @@ object ProtoTypes { /** A class marking ignored prototypes that can be revealed by `deepenProto` */ case class IgnoredProto(ignored: Type) extends UncachedGroundType with MatchAlways { override def revealIgnored = ignored.revealIgnored - override def deepenProto(implicit ctx: Context): Type = ignored + override def deepenProto(implicit ctx: Context): Type = ignored.deepenProto } /** A prototype for expressions [] that are part of a selection operation: From e7086ef7775d6e592bb9c8d96cb9fee6b1fdcd19 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 20:16:02 +0100 Subject: [PATCH 08/13] Fix test --- tests/neg/i5773.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/neg/i5773.scala b/tests/neg/i5773.scala index 6dae20eb3e45..6e98d5e0c93e 100644 --- a/tests/neg/i5773.scala +++ b/tests/neg/i5773.scala @@ -20,7 +20,7 @@ object Main { def f1 = { println(1 appendS 2) // error This should give the following error message: /* -21 | println(1 appendS 2) // error This should give the following error message: +21 | println(1 appendS 2) | ^^^^^^^^^ |value appendS is not a member of Int. |An extension method was tried, but could not be fully constructed: From 30396ea526147f5b552a6013d7318db71171d6bc Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 22:19:56 +0100 Subject: [PATCH 09/13] Go back to only revealing one level of IgnoredProto --- compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 0451384b2270..69e1d95447b8 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -107,7 +107,7 @@ object ProtoTypes { /** A class marking ignored prototypes that can be revealed by `deepenProto` */ case class IgnoredProto(ignored: Type) extends UncachedGroundType with MatchAlways { override def revealIgnored = ignored.revealIgnored - override def deepenProto(implicit ctx: Context): Type = ignored.deepenProto + override def deepenProto(implicit ctx: Context): Type = ignored } /** A prototype for expressions [] that are part of a selection operation: From 4d85a7d2d7ae58f00d0c7a3197a81dde4aa115bd Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 22:23:08 +0100 Subject: [PATCH 10/13] Deepen prototype on AmbiguousImplicits If an inferred argument is ambiguous, try to deepen the prototyoe, and if that succeeds, try again. --- .../src/dotty/tools/dotc/typer/Typer.scala | 23 +++++++++++-------- tests/pos/i5773.scala | 15 +++++++----- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index febf73d7818c..8b8cade165cc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2413,13 +2413,16 @@ class Typer extends Namer def dummyArg(tp: Type) = untpd.Ident(nme.???).withTypeUnchecked(tp) def addImplicitArgs(implicit ctx: Context) = { - def implicitArgs(formals: List[Type], argIndex: Int): List[Tree] = formals match { + def implicitArgs(formals: List[Type], argIndex: Int, pt: Type): List[Tree] = formals match { case Nil => Nil case formal :: formals1 => val arg = inferImplicitArg(formal, tree.span.endPos) arg.tpe match { - case failed: SearchFailureType - if !failed.isInstanceOf[AmbiguousImplicits] && !tree.symbol.hasDefaultParams => + case failed: AmbiguousImplicits => + val pt1 = pt.deepenProto + if ((pt1 `ne` pt) && resultMatches(wtp, pt1)) implicitArgs(formals, argIndex, pt1) + else arg :: implicitArgs(formals1, argIndex + 1, pt1) + case failed: SearchFailureType if !tree.symbol.hasDefaultParams => // no need to search further, the adapt fails in any case // the reason why we continue inferring arguments in case of an AmbiguousImplicits // is that we need to know whether there are further errors. @@ -2433,10 +2436,10 @@ class Typer extends Namer if (wtp.isParamDependent && arg.tpe.exists) formals1.mapconserve(f1 => safeSubstParam(f1, wtp.paramRefs(argIndex), arg.tpe)) else formals1 - arg :: implicitArgs(formals2, argIndex + 1) + arg :: implicitArgs(formals2, argIndex + 1, pt) } } - val args = implicitArgs(wtp.paramInfos, 0) + val args = implicitArgs(wtp.paramInfos, 0, pt) def propagatedFailure(args: List[Tree]): Type = args match { case arg :: args1 => @@ -2612,11 +2615,12 @@ class Typer extends Namer case _ => tp } + def resultMatches(wtp: Type, pt: Type) = + constrainResult(tree.symbol, wtp, revealProtoOfExtMethod(followAlias(pt))) + def adaptNoArgs(wtp: Type): Tree = { val ptNorm = underlyingApplied(pt) def functionExpected = defn.isFunctionType(ptNorm) - def resultMatch = - constrainResult(tree.symbol, wtp, revealProtoOfExtMethod(followAlias(pt))) def needsEta = pt match { case _: SingletonType => false case IgnoredProto(_: FunOrPolyProto) => false @@ -2627,8 +2631,9 @@ class Typer extends Namer case wtp: ExprType => readaptSimplified(tree.withType(wtp.resultType)) case wtp: MethodType if wtp.isImplicitMethod && - ({ resMatch = resultMatch; resMatch } || !functionExpected) => - if (resMatch || ctx.mode.is(Mode.ImplicitsEnabled)) adaptNoArgsImplicitMethod(wtp) + ({ resMatch = resultMatches(wtp, pt); resMatch } || !functionExpected) => + if (resMatch || ctx.mode.is(Mode.ImplicitsEnabled)) + adaptNoArgsImplicitMethod(wtp) else { // Don't proceed with implicit search if result type cannot match - the search // will likely be under-constrained, which means that an unbounded number of alternatives diff --git a/tests/pos/i5773.scala b/tests/pos/i5773.scala index 70fee71ec15f..e799691a249d 100644 --- a/tests/pos/i5773.scala +++ b/tests/pos/i5773.scala @@ -11,23 +11,26 @@ object Semigroup { override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) } - implicit class SumSemigroupDeco[N](implicit N: Numeric[N]) extends Semigroup[N] { + implicit class SumSemiGroupDeco[N](implicit N: Numeric[N]) extends Semigroup[N] { override def (lhs: N) append (rhs: N): N = ??? // N.plus(lhs, rhs) } } - object Main { import Semigroup.sumSemigroup // this is not sufficient def f1 = { import Semigroup.stringAppend // necessary to make the extension method visible - println("Hi" append " mum") // ok - //sumSemigroup.apply(1)(2) - println(1 append 2) // error: this won't compile + println("Hi" append " mum") + println(1 append 2) } def f2 = { implicit val intSumAppend: Semigroup[Int] = sumSemigroup[Int] - println(3 append 4) // this will + println(3 append 4) + } + + def f3 = { + import Semigroup.SumSemiGroupDeco + sumSemigroup.append(1)(2) } } \ No newline at end of file From 73c155a96bcd482caa3bb34e66c0226f0c126c30 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Sun, 3 Feb 2019 22:26:41 +0100 Subject: [PATCH 11/13] Another test --- tests/pos/i5773a.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/pos/i5773a.scala diff --git a/tests/pos/i5773a.scala b/tests/pos/i5773a.scala new file mode 100644 index 000000000000..2071402609c3 --- /dev/null +++ b/tests/pos/i5773a.scala @@ -0,0 +1,17 @@ +trait Semigroup[T] { + def (x: T) combine (y: T): T +} +object Test { + implicit val IntSemigroup: Semigroup[Int] = new { + def (x: Int) combine (y: Int): Int = x + y + } + implicit def OptionSemigroup[T: Semigroup]: Semigroup[Option[T]] = new { + def (x: Option[T]) combine (y: Option[T]): Option[T] = for { + x0 <- x + y0 <- y + } yield x0.combine(y0) + } + 1.combine(2) + Some(1).combine(Some(2)) + Option(1) combine Option(2) +} From fb58de131bfa98e9a15b7c489a91fd9b4ed98e8a Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Mon, 4 Feb 2019 11:08:29 +0100 Subject: [PATCH 12/13] Add comments --- .../src/dotty/tools/dotc/typer/ProtoTypes.scala | 13 +++++++++++-- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala index 69e1d95447b8..474da45bb114 100644 --- a/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala +++ b/compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala @@ -45,7 +45,14 @@ object ProtoTypes { } if (keepConstraint) tp.widenSingleton match { - case poly: PolyType => normalizedCompatible(tp, pt, keepConstraint = false) + case poly: PolyType => + // We can't keep the constraint in this case, since we have to add type parameters + // to it, but there's no place to associate them with type variables. + // So we'd get a "inconsistent: no typevars were added to committable constraint" + // assertion failure in `constrained`. To do better, we'd have to change the + // constraint handling architecture so that some type parameters are committable + // and others are not. But that's a whole different ballgame. + normalizedCompatible(tp, pt, keepConstraint = false) case _ => testCompat } else ctx.test(implicit ctx => testCompat) @@ -246,6 +253,8 @@ object ProtoTypes { def isMatchedBy(tp: Type, keepConstraint: Boolean)(implicit ctx: Context): Boolean = { val args = unforcedTypedArgs def isPoly(tree: Tree) = tree.tpe.widenSingleton.isInstanceOf[PolyType] + // See remark in normalizedCompatible for why we can't keep the constraint + // if one of the arguments has a PolyType. typer.isApplicable(tp, Nil, args, resultType, keepConstraint && !args.exists(isPoly)) } @@ -308,7 +317,7 @@ object ProtoTypes { if (state.typedArgs.size == args.length) state.typedArgs else { val args1 = args.mapconserve(cacheTypedArg(_, typer.typed(_), force)) - if (!args1.contains(WildcardType)) state.typedArgs = args1 + if (force || !args1.contains(WildcardType)) state.typedArgs = args1 args1 } diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 8b8cade165cc..8b792cfac1b2 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -2398,7 +2398,7 @@ class Typer extends Namer } /** Reveal ignored parts of prototype when synthesizing the receiver - * of an extension method. This is necessary for pos/i5773.scala + * of an extension method. This is necessary for pos/i5773a.scala */ def revealProtoOfExtMethod(tp: Type)(implicit ctx: Context): Type = if (ctx.mode.is(Mode.SynthesizeExtMethodReceiver)) tp.deepenProto From 75178c44e773a5514e630b4c4aeeb8306fc6ea80 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Mon, 4 Feb 2019 22:32:34 +0100 Subject: [PATCH 13/13] Update compiler/src/dotty/tools/dotc/typer/Typer.scala Co-Authored-By: odersky --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 8b792cfac1b2..c3b75ee6500f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -75,7 +75,7 @@ object Typer { */ private val DroppedEmptyArgs = new Property.Key[Unit] - /** Am attachment that indicates a failed conversion or extension method + /** An attachment that indicates a failed conversion or extension method * search was tried on a tree. This will in some cases be reported in error messages */ private[typer] val HiddenSearchFailure = new Property.Key[SearchFailure]