Skip to content

Fix #4986: Support implicitNotFound on parameters #9957

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 1 commit into from
Nov 4, 2020
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
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2447,8 +2447,8 @@ import transform.SymUtils._

class InvalidReferenceInImplicitNotFoundAnnotation(typeVar: String, owner: String)(using Context)
extends ReferenceMsg(InvalidReferenceInImplicitNotFoundAnnotationID) {
def msg = em"""|Invalid reference to a type variable "${hl(typeVar)}" found in the annotation argument.
|The variable does not occur in the signature of ${hl(owner)}.
def msg = em"""|Invalid reference to a type variable ${hl(typeVar)} found in the annotation argument.
|The variable does not occur as a parameter in the scope of ${hl(owner)}.
|""".stripMargin
def explain = ""
}
183 changes: 175 additions & 8 deletions compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import ast._
import core._
import Types._, ProtoTypes._, Contexts._, Decorators._, Denotations._, Symbols._
import Implicits._, Flags._, Constants.Constant
import Trees._
import NameOps._
import util.Spans._
import util.SrcPos
import config.Feature
import java.util.regex.Matcher.quoteReplacement
import reporting._

import scala.util.matching.Regex

object ErrorReporting {

import tpd._
Expand Down Expand Up @@ -131,14 +135,6 @@ object ErrorReporting {
* all occurrences of `${X}` where `X` is in `paramNames` with the
* corresponding shown type in `args`.
*/
def userDefinedErrorString(raw: String, paramNames: List[String], args: List[Type]): String = {
def translate(name: String): Option[String] = {
assert(paramNames.length == args.length)
val idx = paramNames.indexOf(name)
if (idx >= 0) Some(quoteReplacement(ex"${args(idx)}")) else None
}
"""\$\{\w*\}""".r.replaceSomeIn(raw, m => translate(m.matched.drop(2).init))
}

def rewriteNotice: String =
if Feature.migrateTo3 then "\nThis patch can be inserted automatically under -rewrite."
Expand Down Expand Up @@ -180,9 +176,180 @@ object ErrorReporting {
end selectErrorAddendum
}

def substitutableTypeSymbolsInScope(sym: Symbol)(using Context): List[Symbol] =
sym.ownersIterator.takeWhile(!_.is(Flags.Package)).flatMap { ownerSym =>
ownerSym.paramSymss.flatten.filter(_.isType) ++
ownerSym.typeRef.nonClassTypeMembers.map(_.symbol)
}.toList

def dependentStr =
"""Term-dependent types are experimental,
|they must be enabled with a `experimental.dependent` language import or setting""".stripMargin

def err(using Context): Errors = new Errors
}


class ImplicitSearchError(
arg: tpd.Tree,
pt: Type,
where: String,
paramSymWithMethodCallTree: Option[(Symbol, tpd.Tree)] = None,
ignoredInstanceNormalImport: => Option[SearchSuccess],
importSuggestionAddendum: => String
)(using ctx: Context) {
def missingArgMsg = arg.tpe match {
case ambi: AmbiguousImplicits =>
(ambi.alt1, ambi.alt2) match {
case (alt @ AmbiguousImplicitMsg(msg), _) =>
userDefinedAmbiguousImplicitMsg(alt, msg)
case (_, alt @ AmbiguousImplicitMsg(msg)) =>
userDefinedAmbiguousImplicitMsg(alt, msg)
case _ =>
defaultAmbiguousImplicitMsg(ambi)
}
case _ =>
val shortMessage = userDefinedImplicitNotFoundParamMessage
.orElse(userDefinedImplicitNotFoundTypeMessage)
.getOrElse(defaultImplicitNotFoundMessage)
formatMsg(shortMessage)() ++ hiddenImplicitsAddendum
}

private def formatMsg(shortForm: String)(headline: String = shortForm) = arg match {
case arg: Trees.SearchFailureIdent[?] =>
shortForm
case _ =>
arg.tpe match {
case tpe: SearchFailureType =>
val original = arg match
case Inlined(call, _, _) => call
case _ => arg

i"""$headline.
|I found:
|
| ${original.show.replace("\n", "\n ")}
|
|But ${tpe.explanation}."""
}
}

private 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(ex"${args(idx)}") else None
}

"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match {
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse(""))
})
}

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

private def location(preposition: String) = if (where.isEmpty) "" else s" $preposition $where"

private def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) = {
formatMsg(s"ambiguous implicit arguments: ${ambi.explanation}${location("of")}")(
s"ambiguous implicit arguments of type ${pt.show} found${location("for")}"
)
}

private def defaultImplicitNotFoundMessage = {
em"no implicit argument of type $pt was found${location("for")}"
}

/** Construct a custom error message given an ambiguous implicit
* candidate `alt` and a user defined message `raw`.
*/
private 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[tpd.Tree])(using Context) =
targs.map(a => Inferencing.fullyDefinedType(a.tpe, "type argument", a.span))

// 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 = tpd.closureBody(alt.tree) // the tree itself if not a closure
val (_, targs, _) = tpd.decomposeCall(call)
val args = resolveTypes(targs)(using ctx.fresh.setTyperState(alt.tstate))
userDefinedErrorString(raw, params, args)
}

/** @param rawMsg Message template with variables, e.g. "Variable A is ${A}"
* @param sym Symbol of the annotated type or of the method whose parameter was annotated
* @param substituteType Function substituting specific types for abstract types associated with variables, e.g A -> Int
*/
private def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = {
val substitutableTypesSymbols = ErrorReporting.substitutableTypeSymbolsInScope(sym)

userDefinedErrorString(
rawMsg,
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
)
}

/** Extracting the message from a method parameter, e.g. in
*
* trait Foo
*
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
*/
private def userDefinedImplicitNotFoundParamMessage = paramSymWithMethodCallTree.flatMap { (sym, applTree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val (fn, targs, _) = tpd.decomposeCall(applTree)
val methodOwner = fn.symbol.owner
val methodOwnerType = tpd.qualifier(fn).tpe
val methodTypeParams = fn.symbol.paramSymss.flatten.filter(_.isType)
val methodTypeArgs = targs.map(_.tpe)
val substituteType = (_: Type).asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs)
formatAnnotationMessage(rawMsg, sym.owner, substituteType)
}
}

/** Extracting the message from a type, e.g. in
*
* @annotation.implicitNotFound("Foo is missing")
* trait Foo
*
* def foo(implicit foo: Foo): Any = ???
*/
private def userDefinedImplicitNotFoundTypeMessage =
val classSym = pt.classSymbol
userDefinedMsg(classSym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val substituteType = (_: Type).asSeenFrom(pt, classSym)
formatAnnotationMessage(rawMsg, classSym, substituteType)
}

private def hiddenImplicitsAddendum: String =
def hiddenImplicitNote(s: SearchSuccess) =
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."

val normalImports = ignoredInstanceNormalImport.map(hiddenImplicitNote)

normalImports.getOrElse(importSuggestionAddendum)
end hiddenImplicitsAddendum

private object AmbiguousImplicitMsg {
def unapply(search: SearchSuccess): Option[String] =
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
}
}
151 changes: 33 additions & 118 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ object Implicits:
def strictEquality(using Context): Boolean =
ctx.mode.is(Mode.StrictEquality) || Feature.enabled(nme.strictEquality)


/** A common base class of contextual implicits and of-type implicits which
* represents a set of references to implicit definitions.
*/
Expand Down Expand Up @@ -832,126 +833,40 @@ trait Implicits:
arg
}

def missingArgMsg(arg: Tree, pt: Type, where: String)(using Context): String = {

def msg(shortForm: String)(headline: String = shortForm) = arg match {
case arg: Trees.SearchFailureIdent[?] =>
shortForm
case _ =>
arg.tpe match {
case tpe: SearchFailureType =>
val original = arg match
case Inlined(call, _, _) => call
case _ => arg

i"""$headline.
|I found:
|
| ${original.show.replace("\n", "\n ")}
|
|But ${tpe.explanation}."""
}
}

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 =>
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
/** @param arg Tree representing a failed result of implicit search
* @param pt Type for which an implicit value was searched
* @param where Description of where the search was performed. Might be empty
* @param paramSymWithMethodCallTree Symbol of the parameter for which the implicit was searched and tree of the method call that triggered the implicit search
*/
def missingArgMsg(
arg: Tree,
pt: Type,
where: String,
paramSymWithMethodCallTree: Option[(Symbol, Tree)] = None
)(using Context): String = {
def findHiddenImplicitsCtx(c: Context): Context =
if c == NoContext then c
else c.freshOver(findHiddenImplicitsCtx(c.outer)).addMode(Mode.FindHiddenImplicits)

def ignoredInstanceNormalImport = arg.tpe match
case fail: SearchFailureType =>
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
inferImplicit(fail.expectedType, fail.argument, arg.span) match {
case s: SearchSuccess => Some(s)
case f: SearchFailure =>
f.reason match {
case ambi: AmbiguousImplicits => Some(ambi.alt1)
case r => None
}
}
def resolveTypes(targs: List[Tree])(using Context) =
targs.map(a => fullyDefinedType(a.tpe, "type argument", a.span))

// 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)(using 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")}")
}
else
// It's unsafe to search for parts of the expected type if they are not fully defined,
// since these come with nested contexts that are lost at this point. See #7249 for an
// example where searching for a nested type causes an infinite loop.
None

case _ =>
val userDefined = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map(raw =>
err.userDefinedErrorString(
raw,
pt.typeSymbol.typeParams.map(_.name.unexpandedName.toString),
pt.widenExpr.dropDependentRefinement.argInfos))

def hiddenImplicitsAddendum: String =

def hiddenImplicitNote(s: SearchSuccess) =
em"\n\nNote: given instance ${s.ref.symbol.showLocated} was not considered because it was not imported with `import given`."

def findHiddenImplicitsCtx(c: Context): Context =
if c == NoContext then c
else c.freshOver(findHiddenImplicitsCtx(c.outer)).addMode(Mode.FindHiddenImplicits)

val normalImports = arg.tpe match
case fail: SearchFailureType =>
if (fail.expectedType eq pt) || isFullyDefined(fail.expectedType, ForceDegree.none) then
inferImplicit(fail.expectedType, fail.argument, arg.span)(
using findHiddenImplicitsCtx(ctx)) match {
case s: SearchSuccess => hiddenImplicitNote(s)
case f: SearchFailure =>
f.reason match {
case ambi: AmbiguousImplicits => hiddenImplicitNote(ambi.alt1)
case r => ""
}
}
else
// It's unsafe to search for parts of the expected type if they are not fully defined,
// since these come with nested contexts that are lost at this point. See #7249 for an
// example where searching for a nested type causes an infinite loop.
""

def suggestedImports = importSuggestionAddendum(pt)
if normalImports.isEmpty then suggestedImports else normalImports
end hiddenImplicitsAddendum

msg(userDefined.getOrElse(
em"no implicit argument of type $pt was found${location("for")}"))() ++
hiddenImplicitsAddendum
}
val error = new ImplicitSearchError(arg, pt, where, paramSymWithMethodCallTree, ignoredInstanceNormalImport, importSuggestionAddendum(pt))
error.missingArgMsg
}

/** A string indicating the formal parameter corresponding to a missing argument */
Expand Down
Loading