Skip to content

Fix #3797: Add support for @implicitAmbiguous #4007

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions compiler/src/dotty/tools/dotc/ast/TreeInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import util.HashSet
import typer.ConstFold
import reporting.trace

import scala.annotation.tailrec

trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] =>
import TreeInfo._

Expand Down Expand Up @@ -512,17 +514,22 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
}

/** Decompose a call fn[targs](vargs_1)...(vargs_n)
* into its constituents (where targs, vargss may be empty)
* into its constituents (fn, targs, vargss).
*
* Note: targ and vargss may be empty
*/
def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = tree match {
case Apply(fn, args) =>
val (meth, targs, argss) = decomposeCall(fn)
(meth, targs, argss :+ args)
case TypeApply(fn, targs) =>
val (meth, targss, args) = decomposeCall(fn)
(meth, targs ++ targss, args)
case _ =>
(tree, Nil, Nil)
def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = {
@tailrec
def loop(tree: Tree, targss: List[Tree], argss: List[List[Tree]]): (Tree, List[Tree], List[List[Tree]]) =
tree match {
case Apply(fn, args) =>
loop(fn, targss, args :: argss)
case TypeApply(fn, targs) =>
loop(fn, targs ::: targss, argss)
case _ =>
(tree, targss, argss)
}
loop(tree, Nil, Nil)
}

/** An extractor for closures, either contained in a block or standalone.
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,8 @@ class Definitions {
def ContravariantBetweenAnnot(implicit ctx: Context) = ContravariantBetweenAnnotType.symbol.asClass
lazy val DeprecatedAnnotType = ctx.requiredClassRef("scala.deprecated")
def DeprecatedAnnot(implicit ctx: Context) = DeprecatedAnnotType.symbol.asClass
lazy val ImplicitAmbiguousAnnotType = ctx.requiredClassRef("scala.annotation.implicitAmbiguous")
def ImplicitAmbiguousAnnot(implicit ctx: Context) = ImplicitAmbiguousAnnotType.symbol.asClass
lazy val ImplicitNotFoundAnnotType = ctx.requiredClassRef("scala.annotation.implicitNotFound")
def ImplicitNotFoundAnnot(implicit ctx: Context) = ImplicitNotFoundAnnotType.symbol.asClass
lazy val InlineAnnotType = ctx.requiredClassRef("scala.inline")
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ object ErrorReporting {
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript)
}

/** Format `raw` implicitNotFound argument, replacing all
* occurrences of `${X}` where `X` is in `paramNames` with the
/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this function might be useful for more annotations (@smarter mentioned a couple more ones on Gitter), as suggested by the new name you picked, but not by the comment. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I sort of agree. I'm open to suggestions 😅

Copy link
Member

Choose a reason for hiding this comment

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

I think it's not worth overgeneralizing, we can change this when we need to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may have misinterpreted your question a little bit. It is useful for all annotations whose messages can reference type parameters, which could potentially be more than the 2 listed here, but not all annotations carrying a custom message. I'm open to suggestions for a better name.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, I suspect the name is fine and would write "Format raw user-defined error message" in the doc. WDYT? Anyway, this isn't critical, just nice to have. (Warning, I've been told I'm a perfectionist).

* all occurrences of `${X}` where `X` is in `paramNames` with the
* corresponding shown type in `args`.
*/
def implicitNotFoundString(raw: String, paramNames: List[String], args: List[Type]): String = {
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
def translate(name: String): Option[String] = {
val idx = paramNames.indexOf(name)
if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None
Expand Down
76 changes: 61 additions & 15 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Constants._
import Applications._
import ProtoTypes._
import ErrorReporting._
import Annotations.Annotation
import reporting.diagnostic.{Message, MessageContainer}
import Inferencing.fullyDefinedType
import Trees._
Expand Down Expand Up @@ -545,7 +546,7 @@ trait Implicits { self: Typer =>

/** If `formal` is of the form ClassTag[T], where `T` is a class type,
* synthesize a class tag for `T`.
*/
*/
def synthesizedClassTag(formal: Type): Tree = formal.argInfos match {
case arg :: Nil =>
fullyDefinedType(arg, "ClassTag argument", pos) match {
Expand Down Expand Up @@ -590,7 +591,7 @@ trait Implicits { self: Typer =>

/** If `formal` is of the form Eq[T, U], where no `Eq` instance exists for
* either `T` or `U`, synthesize `Eq.eqAny[T, U]` as solution.
*/
*/
def synthesizedEq(formal: Type)(implicit ctx: Context): Tree = {
//println(i"synth eq $formal / ${formal.argTypes}%, %")
formal.argTypes match {
Expand Down Expand Up @@ -685,22 +686,67 @@ trait Implicits { self: Typer =>
}
}
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"

/** Extract a user defined error message from a symbol `sym`
* with an annotation matching the given class symbol `cls`.
*/
def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
ann <- sym.getAnnotation(cls)
Trees.Literal(Constant(msg: String)) <- ann.argument(0)
} yield msg


arg.tpe match {
case ambi: AmbiguousImplicits =>
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
case _ =>
val userDefined =
for {
notFound <- pt.typeSymbol.getAnnotation(defn.ImplicitNotFoundAnnot)
Trees.Literal(Constant(raw: String)) <- notFound.argument(0)
}
yield {
err.implicitNotFoundString(
raw,
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
pt.argInfos)
object AmbiguousImplicitMsg {
def unapply(search: SearchSuccess): Option[String] =
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
}

/** Construct a custom error message given an ambiguous implicit
* candidate `alt` and a user defined message `raw`.
*/
def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = {
val params = alt.ref.underlying match {
case p: PolyType => p.paramNames.map(_.toString)
case _ => Nil
}
def resolveTypes(targs: List[Tree])(implicit ctx: Context) =
targs.map(a => fullyDefinedType(a.tpe, "type argument", a.pos))

// We can extract type arguments from:
// - a function call:
// @implicitAmbiguous("msg A=${A}")
// implicit def f[A](): String = ...
// implicitly[String] // found: f[Any]()
//
// - an eta-expanded function:
// @implicitAmbiguous("msg A=${A}")
// implicit def f[A](x: Int): String = ...
// implicitly[Int => String] // found: x => f[Any](x)

val call = closureBody(alt.tree) // the tree itself if not a closure
val (_, targs, _) = decomposeCall(call)
val args = resolveTypes(targs)(ctx.fresh.setTyperState(alt.tstate))
err.userDefinedErrorString(raw, params, args)
}

(ambi.alt1, ambi.alt2) match {
case (alt @ AmbiguousImplicitMsg(msg), _) =>
userDefinedAmbiguousImplicitMsg(alt, msg)
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
userDefinedAmbiguousImplicitMsg(alt, msg)
case _ =>
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
}

case _ =>
val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw =>
err.userDefinedErrorString(
raw,
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
pt.argInfos))
msg(userDefined.getOrElse(em"no implicit argument of type $pt was found${location("for")}"))()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package dotty.tools
package dotc
package reporting

import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.reporting.diagnostic.messages._
import org.junit.Assert._
import org.junit.Test

class UserDefinedErrorMessages extends ErrorMessagesTest {
@Test def userDefinedImplicitAmbiguous1 =
checkMessagesAfter("frontend") {
"""
|object Test {
| trait =!=[C, D]
|
| implicit def neq[E, F] : E =!= F = null
|
| @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}")
| implicit def neqAmbig1[G, H, J] : J =!= J = null
| implicit def neqAmbig2[I] : I =!= I = null
|
| implicitly[Int =!= Int]
|}
""".stripMargin
}.expect { (itcx, messages) =>
import diagnostic.NoExplanation
implicit val ctx: Context = itcx

assertMessageCount(1, messages)
val (m: NoExplanation) :: Nil = messages

assertEquals(m.msg, "Could not prove Int =!= Int")
}

@Test def userDefinedImplicitAmbiguous2 =
checkMessagesAfter("frontend") {
"""
|object Test {
| trait =!=[C, D]
|
| implicit def neq[E, F] : E =!= F = null
|
| implicit def neqAmbig1[G, H, J] : J =!= J = null
| @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}")
| implicit def neqAmbig2[I] : I =!= I = null
|
| implicitly[Int =!= Int]
|}
""".stripMargin
}.expect { (itcx, messages) =>
import diagnostic.NoExplanation
implicit val ctx: Context = itcx

assertMessageCount(1, messages)
val (m: NoExplanation) :: Nil = messages

assertEquals(m.msg, "Could not prove Int =!= Int")
}

@Test def userDefinedImplicitAmbiguous3 =
checkMessagesAfter("frontend") {
"""
|object Test {
| trait =!=[C, D]
|
| implicit def neq[E, F] : E =!= F = null
|
| @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}")
| implicit def neqAmbig1[G, H, J] : J =!= J = null
| @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}")
| implicit def neqAmbig2[I] : I =!= I = null
|
| implicitly[Int =!= Int]
|}
""".stripMargin
}.expect { (itcx, messages) =>
import diagnostic.NoExplanation
implicit val ctx: Context = itcx

assertMessageCount(1, messages)
val (m: NoExplanation) :: Nil = messages

assertEquals(m.msg, "Could not prove Int =!= Int")
}

@Test def userDefinedImplicitAmbiguous4 =
checkMessagesAfter("frontend") {
"""
|class C {
| @annotation.implicitAmbiguous("msg A=${A}")
| implicit def f[A](x: Int): String = "f was here"
| implicit def g(x: Int): String = "f was here"
| def test: Unit = {
| implicitly[Int => String]
| }
|}
""".stripMargin
}.expect { (itcx, messages) =>
import diagnostic.NoExplanation
implicit val ctx: Context = itcx

assertMessageCount(1, messages)
val (m: NoExplanation) :: Nil = messages

assertEquals(m.msg, "msg A=Any")
}

@Test def userDefinedImplicitAmbiguous5 =
checkMessagesAfter("frontend") {
"""
|class C {
| @annotation.implicitAmbiguous("msg A=${A}")
| implicit def f[A](implicit x: String): Int = 1
| implicit def g: Int = 2
| def test: Unit = {
| implicit val s: String = "Hello"
| implicitly[Int]
| }
|}
""".stripMargin
}.expect { (itcx, messages) =>
import diagnostic.NoExplanation
implicit val ctx: Context = itcx

assertMessageCount(1, messages)
val (m: NoExplanation) :: Nil = messages

assertEquals(m.msg, "msg A=Any")
}
}