Skip to content

Commit 0c5e39d

Browse files
authored
Merge pull request #4007 from Jasper-M/topic/implicit-ambiguous
Fix #3797: Add support for @implicitAmbiguous
2 parents 0eb0423 + 5a55c33 commit 0c5e39d

File tree

5 files changed

+214
-28
lines changed

5 files changed

+214
-28
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import util.HashSet
99
import typer.ConstFold
1010
import reporting.trace
1111

12+
import scala.annotation.tailrec
13+
1214
trait TreeInfo[T >: Untyped <: Type] { self: Trees.Instance[T] =>
1315
import TreeInfo._
1416

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

514516
/** Decompose a call fn[targs](vargs_1)...(vargs_n)
515-
* into its constituents (where targs, vargss may be empty)
517+
* into its constituents (fn, targs, vargss).
518+
*
519+
* Note: targ and vargss may be empty
516520
*/
517-
def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = tree match {
518-
case Apply(fn, args) =>
519-
val (meth, targs, argss) = decomposeCall(fn)
520-
(meth, targs, argss :+ args)
521-
case TypeApply(fn, targs) =>
522-
val (meth, targss, args) = decomposeCall(fn)
523-
(meth, targs ++ targss, args)
524-
case _ =>
525-
(tree, Nil, Nil)
521+
def decomposeCall(tree: Tree): (Tree, List[Tree], List[List[Tree]]) = {
522+
@tailrec
523+
def loop(tree: Tree, targss: List[Tree], argss: List[List[Tree]]): (Tree, List[Tree], List[List[Tree]]) =
524+
tree match {
525+
case Apply(fn, args) =>
526+
loop(fn, targss, args :: argss)
527+
case TypeApply(fn, targs) =>
528+
loop(fn, targs ::: targss, argss)
529+
case _ =>
530+
(tree, targss, argss)
531+
}
532+
loop(tree, Nil, Nil)
526533
}
527534

528535
/** An extractor for closures, either contained in a block or standalone.

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@ class Definitions {
679679
def ContravariantBetweenAnnot(implicit ctx: Context) = ContravariantBetweenAnnotType.symbol.asClass
680680
lazy val DeprecatedAnnotType = ctx.requiredClassRef("scala.deprecated")
681681
def DeprecatedAnnot(implicit ctx: Context) = DeprecatedAnnotType.symbol.asClass
682+
lazy val ImplicitAmbiguousAnnotType = ctx.requiredClassRef("scala.annotation.implicitAmbiguous")
683+
def ImplicitAmbiguousAnnot(implicit ctx: Context) = ImplicitAmbiguousAnnotType.symbol.asClass
682684
lazy val ImplicitNotFoundAnnotType = ctx.requiredClassRef("scala.annotation.implicitNotFound")
683685
def ImplicitNotFoundAnnot(implicit ctx: Context) = ImplicitNotFoundAnnotType.symbol.asClass
684686
lazy val InlineAnnotType = ctx.requiredClassRef("scala.inline")

compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,11 @@ object ErrorReporting {
160160
TypeMismatch(found2, expected2, whyNoMatchStr(found, expected), postScript)
161161
}
162162

163-
/** Format `raw` implicitNotFound argument, replacing all
164-
* occurrences of `${X}` where `X` is in `paramNames` with the
163+
/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
164+
* all occurrences of `${X}` where `X` is in `paramNames` with the
165165
* corresponding shown type in `args`.
166166
*/
167-
def implicitNotFoundString(raw: String, paramNames: List[String], args: List[Type]): String = {
167+
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
168168
def translate(name: String): Option[String] = {
169169
val idx = paramNames.indexOf(name)
170170
if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None

compiler/src/dotty/tools/dotc/typer/Implicits.scala

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Constants._
2626
import Applications._
2727
import ProtoTypes._
2828
import ErrorReporting._
29+
import Annotations.Annotation
2930
import reporting.diagnostic.{Message, MessageContainer}
3031
import Inferencing.fullyDefinedType
3132
import Trees._
@@ -545,7 +546,7 @@ trait Implicits { self: Typer =>
545546

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

591592
/** If `formal` is of the form Eq[T, U], where no `Eq` instance exists for
592593
* either `T` or `U`, synthesize `Eq.eqAny[T, U]` as solution.
593-
*/
594+
*/
594595
def synthesizedEq(formal: Type)(implicit ctx: Context): Tree = {
595596
//println(i"synth eq $formal / ${formal.argTypes}%, %")
596597
formal.argTypes match {
@@ -685,22 +686,67 @@ trait Implicits { self: Typer =>
685686
}
686687
}
687688
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"
689+
690+
/** Extract a user defined error message from a symbol `sym`
691+
* with an annotation matching the given class symbol `cls`.
692+
*/
693+
def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
694+
ann <- sym.getAnnotation(cls)
695+
Trees.Literal(Constant(msg: String)) <- ann.argument(0)
696+
} yield msg
697+
698+
688699
arg.tpe match {
689700
case ambi: AmbiguousImplicits =>
690-
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
691-
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
692-
case _ =>
693-
val userDefined =
694-
for {
695-
notFound <- pt.typeSymbol.getAnnotation(defn.ImplicitNotFoundAnnot)
696-
Trees.Literal(Constant(raw: String)) <- notFound.argument(0)
697-
}
698-
yield {
699-
err.implicitNotFoundString(
700-
raw,
701-
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
702-
pt.argInfos)
701+
object AmbiguousImplicitMsg {
702+
def unapply(search: SearchSuccess): Option[String] =
703+
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
704+
}
705+
706+
/** Construct a custom error message given an ambiguous implicit
707+
* candidate `alt` and a user defined message `raw`.
708+
*/
709+
def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = {
710+
val params = alt.ref.underlying match {
711+
case p: PolyType => p.paramNames.map(_.toString)
712+
case _ => Nil
703713
}
714+
def resolveTypes(targs: List[Tree])(implicit ctx: Context) =
715+
targs.map(a => fullyDefinedType(a.tpe, "type argument", a.pos))
716+
717+
// We can extract type arguments from:
718+
// - a function call:
719+
// @implicitAmbiguous("msg A=${A}")
720+
// implicit def f[A](): String = ...
721+
// implicitly[String] // found: f[Any]()
722+
//
723+
// - an eta-expanded function:
724+
// @implicitAmbiguous("msg A=${A}")
725+
// implicit def f[A](x: Int): String = ...
726+
// implicitly[Int => String] // found: x => f[Any](x)
727+
728+
val call = closureBody(alt.tree) // the tree itself if not a closure
729+
val (_, targs, _) = decomposeCall(call)
730+
val args = resolveTypes(targs)(ctx.fresh.setTyperState(alt.tstate))
731+
err.userDefinedErrorString(raw, params, args)
732+
}
733+
734+
(ambi.alt1, ambi.alt2) match {
735+
case (alt @ AmbiguousImplicitMsg(msg), _) =>
736+
userDefinedAmbiguousImplicitMsg(alt, msg)
737+
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
738+
userDefinedAmbiguousImplicitMsg(alt, msg)
739+
case _ =>
740+
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
741+
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
742+
}
743+
744+
case _ =>
745+
val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw =>
746+
err.userDefinedErrorString(
747+
raw,
748+
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
749+
pt.argInfos))
704750
msg(userDefined.getOrElse(em"no implicit argument of type $pt was found${location("for")}"))()
705751
}
706752
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package dotty.tools
2+
package dotc
3+
package reporting
4+
5+
import dotty.tools.dotc.core.Contexts.Context
6+
import dotty.tools.dotc.reporting.diagnostic.messages._
7+
import org.junit.Assert._
8+
import org.junit.Test
9+
10+
class UserDefinedErrorMessages extends ErrorMessagesTest {
11+
@Test def userDefinedImplicitAmbiguous1 =
12+
checkMessagesAfter("frontend") {
13+
"""
14+
|object Test {
15+
| trait =!=[C, D]
16+
|
17+
| implicit def neq[E, F] : E =!= F = null
18+
|
19+
| @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}")
20+
| implicit def neqAmbig1[G, H, J] : J =!= J = null
21+
| implicit def neqAmbig2[I] : I =!= I = null
22+
|
23+
| implicitly[Int =!= Int]
24+
|}
25+
""".stripMargin
26+
}.expect { (itcx, messages) =>
27+
import diagnostic.NoExplanation
28+
implicit val ctx: Context = itcx
29+
30+
assertMessageCount(1, messages)
31+
val (m: NoExplanation) :: Nil = messages
32+
33+
assertEquals(m.msg, "Could not prove Int =!= Int")
34+
}
35+
36+
@Test def userDefinedImplicitAmbiguous2 =
37+
checkMessagesAfter("frontend") {
38+
"""
39+
|object Test {
40+
| trait =!=[C, D]
41+
|
42+
| implicit def neq[E, F] : E =!= F = null
43+
|
44+
| implicit def neqAmbig1[G, H, J] : J =!= J = null
45+
| @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}")
46+
| implicit def neqAmbig2[I] : I =!= I = null
47+
|
48+
| implicitly[Int =!= Int]
49+
|}
50+
""".stripMargin
51+
}.expect { (itcx, messages) =>
52+
import diagnostic.NoExplanation
53+
implicit val ctx: Context = itcx
54+
55+
assertMessageCount(1, messages)
56+
val (m: NoExplanation) :: Nil = messages
57+
58+
assertEquals(m.msg, "Could not prove Int =!= Int")
59+
}
60+
61+
@Test def userDefinedImplicitAmbiguous3 =
62+
checkMessagesAfter("frontend") {
63+
"""
64+
|object Test {
65+
| trait =!=[C, D]
66+
|
67+
| implicit def neq[E, F] : E =!= F = null
68+
|
69+
| @annotation.implicitAmbiguous("Could not prove ${J} =!= ${J}")
70+
| implicit def neqAmbig1[G, H, J] : J =!= J = null
71+
| @annotation.implicitAmbiguous("Could not prove ${I} =!= ${I}")
72+
| implicit def neqAmbig2[I] : I =!= I = null
73+
|
74+
| implicitly[Int =!= Int]
75+
|}
76+
""".stripMargin
77+
}.expect { (itcx, messages) =>
78+
import diagnostic.NoExplanation
79+
implicit val ctx: Context = itcx
80+
81+
assertMessageCount(1, messages)
82+
val (m: NoExplanation) :: Nil = messages
83+
84+
assertEquals(m.msg, "Could not prove Int =!= Int")
85+
}
86+
87+
@Test def userDefinedImplicitAmbiguous4 =
88+
checkMessagesAfter("frontend") {
89+
"""
90+
|class C {
91+
| @annotation.implicitAmbiguous("msg A=${A}")
92+
| implicit def f[A](x: Int): String = "f was here"
93+
| implicit def g(x: Int): String = "f was here"
94+
| def test: Unit = {
95+
| implicitly[Int => String]
96+
| }
97+
|}
98+
""".stripMargin
99+
}.expect { (itcx, messages) =>
100+
import diagnostic.NoExplanation
101+
implicit val ctx: Context = itcx
102+
103+
assertMessageCount(1, messages)
104+
val (m: NoExplanation) :: Nil = messages
105+
106+
assertEquals(m.msg, "msg A=Any")
107+
}
108+
109+
@Test def userDefinedImplicitAmbiguous5 =
110+
checkMessagesAfter("frontend") {
111+
"""
112+
|class C {
113+
| @annotation.implicitAmbiguous("msg A=${A}")
114+
| implicit def f[A](implicit x: String): Int = 1
115+
| implicit def g: Int = 2
116+
| def test: Unit = {
117+
| implicit val s: String = "Hello"
118+
| implicitly[Int]
119+
| }
120+
|}
121+
""".stripMargin
122+
}.expect { (itcx, messages) =>
123+
import diagnostic.NoExplanation
124+
implicit val ctx: Context = itcx
125+
126+
assertMessageCount(1, messages)
127+
val (m: NoExplanation) :: Nil = messages
128+
129+
assertEquals(m.msg, "msg A=Any")
130+
}
131+
}

0 commit comments

Comments
 (0)