-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Have an awesome day! ☀️
raw, | ||
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), | ||
pt.argInfos) | ||
alt.tstate.commit() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should try avoiding committing a TyperState with errors if possible. Instead you might be able to make a new Context with the correct TyperState to report the error: val altCtx = ctx.fresh.setTyperState(alt.tstate)
, and then move the code below to be in a method that takes an implicit ctx: Context
and give it altCtx
as argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
I suggest just making a case class for the error, it doesn't have to be very fancy, e.g. it's okay if |
Could you open an issue for that? |
That would be a general |
It's actually a warning apparently. But I'll open an issue. |
Sure, but you could have an AmbiguousImplicit subclass or something. |
@@ -696,22 +697,46 @@ trait Implicits { self: Typer => | |||
} | |||
} | |||
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where" | |||
def userDefinedMessage(annot: Annotation, params: List[String], args: List[Type]): Option[String] = | |||
for(Trees.Literal(Constant(raw: String)) <- annot.argument(0)) yield { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
space after for
case TypeApply(_, targs) if params.nonEmpty => | ||
def resolveTypes(targs: List[tpd.Tree])(implicit ctx: Context) = { | ||
targs.map(a => fullyDefinedType(a.tpe, "type parameter", a.pos)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for braces since the body is an expression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it was better for readability in this case, but your call.
I added tests, but I think it's better if I don't add a half assed custom |
@@ -159,11 +159,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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 😅
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
@@ -696,22 +697,45 @@ trait Implicits { self: Typer => | |||
} | |||
} | |||
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where" | |||
def userDefinedMessage(annot: Annotation, params: List[String], args: List[Type]): Option[String] = | |||
for (Trees.Literal(Constant(raw: String)) <- annot.argument(0)) yield { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering about the magic constant 0
, I guess because the string is the 0th argument for all annotations currently supported? But if you try to make userDefinedMessage
a good abstraction, maybe annot.argument(0)
should move to the call site (even if, indeed, it gets duplicated).
ambi.alt2.ref.symbol.getAnnotation(defn.ImplicitAmbiguousAnnot).map( | ||
(_, ambi.alt2) | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, can this be abstracted with for { alt <- List(ambi.alt1, ambi.alt2) } .... alt.ref.symbol...
? Or better, in terms of a new ambi.alts
?
(Except if people feel this is too slow and we're in a performance-critical path).
Actually surprised the error only lists two implicits, apparently since this class' birth (in ea640a3) only the first two candidate implicits are reported.
Potential bug (not blocking the PR): if you have three equally good candidates that are ambiguous, the @implicitAmbiguous
annotation will be ignored on the third candidate, which is against the specification in http://www.scala-lang.org/api/2.12.3/scala/annotation/implicitAmbiguous.html. If that bug can indeed be triggered (haven't tried, but it's not clear why that shouldn't happen), fixing it seems to require a refactoring of the search algorithm to return all the ambiguous candidates — which is, again, out of scope for the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I also thought about this. I think the Scalac implementation has the same limitation. I suspect the most realistic fix would probably be in the documentation of @implicitAmbiguous
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, fair enough — this wasn't a well-known issue around here. Not sure how to specify it,
so for now I've opened scala/bug#10739?
For the PR, please go ahead!
val userDefined = maybeAnnot.flatMap { case (annot, alt) => | ||
val params = alt.ref.underlying match { | ||
case lambda: TypeLambda => lambda.typeParams.map(_.paramName.unexpandedName.toString) | ||
case _ => Nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@allanrenucci Couldn't quite review code from here on, please take a look. Otherwise LGTM.
) | ||
val userDefined = maybeAnnot.flatMap { case (annot, alt) => | ||
val params = alt.ref.underlying match { | ||
case lambda: TypeLambda => lambda.typeParams.map(_.paramName.unexpandedName.toString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do:
case p: PolyType => p.paramNames.map(_.toString)
pt.argInfos) | ||
val args = alt.tree match { | ||
case TypeApply(_, targs) if params.nonEmpty => | ||
def resolveTypes(targs: List[tpd.Tree])(implicit ctx: Context) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove the tpd
prefix
} | ||
userDefinedMessage(annot, params, args) | ||
} | ||
userDefined.map(msg(_)()).getOrElse( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here .map(msg(_)())
does nothing and can be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's very likely. Is the same true for the ImplicitNotFound
case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I am not sure anymore. I revert what I said before. Let's be conservative and also wrap the user defined message in msg(_)()
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), | ||
pt.argInfos) | ||
val args = alt.tree match { | ||
case TypeApply(_, targs) if params.nonEmpty => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ambi.alt2.ref.symbol.getAnnotation(defn.ImplicitAmbiguousAnnot).map( | ||
(_, ambi.alt2) | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do it the other way around. First look for a user defined annotation message and then try try to recover type params only if needed:
def userDefinedMsg(sym: Symbol, cls: Symbol) = for {
ann <- symbol.getAnnotation(cls)
Trees.Literal(Constant(msg: String)) <- ann.argument(0)
} yield msg
object AmbiguousImplicitMsg {
def unapply(search: SearchSuccess) =
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
}
(ambi.alt1, ambi.alt2) match {
case (AmbiguousImplicitMsg(msg), _) => ... // recover type params from alt1
case (_, AmbiguousImplicitMsg(msg)) => ... // recover type params from alt2
case _ =>
msg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}")
}
How are you supposed to handle Am I just supposed to |
93e7013
to
4261c6a
Compare
4261c6a
to
a550d09
Compare
As far as I'm concerned this is pretty much ready (except for the cast maybe). Please let me know if there's still something that is blocking. Or if there's a better way to handle the LazyTree thing. |
@Jasper-M what about the CI failure? |
Not a clue. There should be no difference between this commit and the one that previously succeeded. |
OK, we’ll take a look on our side. Delays are possible, but we’ll do our best. |
(Build restarted, it was weird that only test failed.) |
a550d09
to
d656ff2
Compare
cbe5d9c
to
a1732a7
Compare
e0ffb5e
to
9498dee
Compare
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString), | ||
pt.argInfos) | ||
object AmbiguousImplicitMsg { | ||
def unapply(search: SearchSuccess) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Write down the result type of this method for clarity.
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot) | ||
} | ||
|
||
def userDefinedAmbiguousImplicitMsg(alt: SearchSuccess, raw: String) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a documentation comment for this method.
@@ -685,22 +686,54 @@ trait Implicits { self: Typer => | |||
} | |||
} | |||
def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where" | |||
|
|||
def userDefinedMsg(sym: Symbol, cls: Symbol) = for { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a documentation comment for this method.
} | ||
|
||
(ambi.alt1, ambi.alt2) match { | ||
case (AmbiguousImplicitMsg(msg), _) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you write alt @ AmbiguousImplicitMsg(msg)
, you can use alt
below instead of ambi.alt1
, same in the next case.
9498dee
to
86ecede
Compare
Merging! Thanks @Jasper-M 🎉 |
There are no tests yet. Can I add a test to https://github.com/lampepfl/dotty/blob/master/compiler/test/dotty/tools/dotc/reporting/ErrorMessagesTests.scala if this error doesn't have a dedicated case class yet?
Dotc will not warn like scalac when the user defined error message references a non-existent type parameter, but that's consistent with @implicitNotFound in dotty.