Skip to content

Commit

Permalink
Merge pull request #4007 from Jasper-M/topic/implicit-ambiguous
Browse files Browse the repository at this point in the history
Fix #3797: Add support for @implicitAmbiguous
  • Loading branch information
allanrenucci committed Mar 21, 2018
2 parents 0eb0423 + 5a55c33 commit 0c5e39d
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 28 deletions.
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
* 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")
}
}

0 comments on commit 0c5e39d

Please sign in to comment.