Skip to content

Commit

Permalink
Allow @implicitNotFound messages as explanations (#16893)
Browse files Browse the repository at this point in the history
A problem of the @implicitNotFOund mechanism so far was that the user
defined message replaced the compiler-generated one, which might lose
valuable information.

This commit adds an alternative where an @implicitNotFound message that
starts with `...` is taken as an explanation (without the ...) enabled
under -explain. The compiler-generated message is then kept as the
explicit error message.

We apply the mechanism for an @implicitNotFound message for
`boundary.Label`. This now produces messages like this one:
```
-- [E172] Type Error: tests/neg-custom-args/explain/labelNotFound.scala:2:30 -------------------------------------------
2 |  scala.util.boundary.break(1) // error
  |                              ^
  |No given instance of type scala.util.boundary.Label[Int] was found for parameter label of method break in object boundary
  |---------------------------------------------------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | A Label is generated from an enclosing `scala.util.boundary` call.
  | Maybe that boundary is missing?
   ---------------------------------------------------------------------------------------------------------------------
```
  • Loading branch information
nicolasstucki authored Mar 9, 2023
2 parents c466fa0 + 4b83f1f commit fd91ce1
Show file tree
Hide file tree
Showing 19 changed files with 183 additions and 127 deletions.
194 changes: 104 additions & 90 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,107 @@ class MissingImplicitArgument(
case ambi: AmbiguousImplicits => withoutDisambiguation()
case _ =>

/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
* 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])(using Context): String =
def translate(name: String): Option[String] =
val idx = paramNames.indexOf(name)
if (idx >= 0) Some(i"${args(idx)}") else None
"""\$\{\s*([^}\s]+)\s*\}""".r.replaceAllIn(raw, (_: Regex.Match) match
case Regex.Groups(v) => quoteReplacement(translate(v).getOrElse("")).nn
)

/** @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
*/
def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type)(using Context): String =
val substitutableTypesSymbols = substitutableTypeSymbolsInScope(sym)
userDefinedErrorString(
rawMsg,
paramNames = substitutableTypesSymbols.map(_.name.unexpandedName.toString),
args = substitutableTypesSymbols.map(_.typeRef).map(substituteType)
)

/** 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)(using Context) =
for
ann <- sym.getAnnotation(cls)
msg <- ann.argumentConstantString(0)
yield msg

def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol)(using Context): Option[String] =
for
rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot)
if Feature.migrateTo3 || sym != defn.Function1
// Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore
yield
val substituteType = (_: Type).asSeenFrom(pt, sym)
formatAnnotationMessage(rawMsg, sym, substituteType)

/** Extracting the message from a method parameter, e.g. in
*
* trait Foo
*
* def foo(implicit @annotation.implicitNotFound("Foo is missing") foo: Foo): Any = ???
*/
def userDefinedImplicitNotFoundParamMessage(using Context): Option[String] =
paramSymWithMethodCallTree.flatMap: (sym, applTree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map: rawMsg =>
val fn = tpd.funPart(applTree)
val targs = tpd.typeArgss(applTree).flatten
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)

def userDefinedImplicitNotFoundTypeMessage(using Context): Option[String] =
def recur(tp: Type): Option[String] = tp match
case tp: TypeRef =>
val sym = tp.symbol
userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info))
case tp: ClassInfo =>
tp.baseClasses.iterator
.map(userDefinedImplicitNotFoundTypeMessageFor)
.find(_.isDefined).flatten
case tp: TypeProxy =>
recur(tp.superType)
case tp: AndType =>
recur(tp.tp1).orElse(recur(tp.tp2))
case _ =>
None
recur(pt)

/** The implicitNotFound annotation on the parameter, or else on the type.
* implicitNotFound message strings starting with `explain=` are intended for
* additional explanations, not the message proper. The leading `explain=` is
* dropped in this case.
* @param explain The message is used for an additional explanation, not
* the message proper.
*/
def userDefinedImplicitNotFoundMessage(explain: Boolean)(using Context): Option[String] =
val explainTag = "explain="
def filter(msg: Option[String]) = msg match
case Some(str) =>
if str.startsWith(explainTag) then
if explain then Some(str.drop(explainTag.length)) else None
else if explain then None
else msg
case None => None
filter(userDefinedImplicitNotFoundParamMessage)
.orElse(filter(userDefinedImplicitNotFoundTypeMessage))

object AmbiguousImplicitMsg {
def unapply(search: SearchSuccess): Option[String] =
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
}

def msg(using Context): String =

def formatMsg(shortForm: String)(headline: String = shortForm) = arg match
Expand All @@ -2596,29 +2697,6 @@ class MissingImplicitArgument(
|But ${tpe.explanation}."""
case _ => headline

/** Format `raw` implicitNotFound or implicitAmbiguous argument, replacing
* 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] = {
val idx = paramNames.indexOf(name)
if (idx >= 0) Some(i"${args(idx)}") else None
}

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

/** 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)
msg <- ann.argumentConstantString(0)
} yield msg

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

def defaultAmbiguousImplicitMsg(ambi: AmbiguousImplicits) =
Expand Down Expand Up @@ -2655,77 +2733,13 @@ class MissingImplicitArgument(
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
*/
def formatAnnotationMessage(rawMsg: String, sym: Symbol, substituteType: Type => Type): String = {
val substitutableTypesSymbols = 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 = ???
*/
def userDefinedImplicitNotFoundParamMessage: Option[String] = paramSymWithMethodCallTree.flatMap { (sym, applTree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val fn = tpd.funPart(applTree)
val targs = tpd.typeArgss(applTree).flatten
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 = ???
*/
def userDefinedImplicitNotFoundTypeMessage: Option[String] =
def recur(tp: Type): Option[String] = tp match
case tp: TypeRef =>
val sym = tp.symbol
userDefinedImplicitNotFoundTypeMessageFor(sym).orElse(recur(tp.info))
case tp: ClassInfo =>
tp.baseClasses.iterator
.map(userDefinedImplicitNotFoundTypeMessageFor)
.find(_.isDefined).flatten
case tp: TypeProxy =>
recur(tp.superType)
case tp: AndType =>
recur(tp.tp1).orElse(recur(tp.tp2))
case _ =>
None
recur(pt)

def userDefinedImplicitNotFoundTypeMessageFor(sym: Symbol): Option[String] =
for
rawMsg <- userDefinedMsg(sym, defn.ImplicitNotFoundAnnot)
if Feature.migrateTo3 || sym != defn.Function1
// Don't inherit "No implicit view available..." message if subtypes of Function1 are not treated as implicit conversions anymore
yield
val substituteType = (_: Type).asSeenFrom(pt, sym)
formatAnnotationMessage(rawMsg, sym, substituteType)

object AmbiguousImplicitMsg {
def unapply(search: SearchSuccess): Option[String] =
userDefinedMsg(search.ref.symbol, defn.ImplicitAmbiguousAnnot)
}

arg.tpe match
case ambi: AmbiguousImplicits =>
(ambi.alt1, ambi.alt2) match
Expand All @@ -2739,8 +2753,7 @@ class MissingImplicitArgument(
i"""No implicit search was attempted${location("for")}
|since the expected type $target is not specific enough"""
case _ =>
val shortMessage = userDefinedImplicitNotFoundParamMessage
.orElse(userDefinedImplicitNotFoundTypeMessage)
val shortMessage = userDefinedImplicitNotFoundMessage(explain = false)
.getOrElse(defaultImplicitNotFoundMessage)
formatMsg(shortMessage)()
end msg
Expand Down Expand Up @@ -2769,7 +2782,8 @@ class MissingImplicitArgument(
.orElse(noChainConversionsNote(ignoredConvertibleImplicits))
.getOrElse(ctx.typer.importSuggestionAddendum(pt))

def explain(using Context) = ""
def explain(using Context) = userDefinedImplicitNotFoundMessage(explain = true)
.getOrElse("")
end MissingImplicitArgument

class CannotBeAccessed(tpe: NamedType, superAccess: Boolean)(using Context)
Expand Down
5 changes: 1 addition & 4 deletions compiler/test/dotty/tools/dotc/CompilationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class CompilationTests {
compileFilesInDir("tests/neg-custom-args/feature", defaultOptions.and("-Xfatal-warnings", "-feature")),
compileFilesInDir("tests/neg-custom-args/no-experimental", defaultOptions.and("-Yno-experimental")),
compileFilesInDir("tests/neg-custom-args/captures", defaultOptions.and("-language:experimental.captureChecking")),
compileFilesInDir("tests/neg-custom-args/explain", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/avoid-warn-deprecation.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
compileFile("tests/neg-custom-args/i3246.scala", scala2CompatMode),
compileFile("tests/neg-custom-args/overrideClass.scala", scala2CompatMode),
Expand All @@ -155,9 +156,6 @@ class CompilationTests {
compileFile("tests/neg-custom-args/i1754.scala", allowDeepSubtypes),
compileFile("tests/neg-custom-args/i12650.scala", allowDeepSubtypes),
compileFile("tests/neg-custom-args/i9517.scala", defaultOptions.and("-Xprint-types")),
compileFile("tests/neg-custom-args/i11637.scala", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/i15575.scala", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/i16601a.scala", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/interop-polytypes.scala", allowDeepSubtypes.and("-Yexplicit-nulls")),
compileFile("tests/neg-custom-args/conditionalWarnings.scala", allowDeepSubtypes.and("-deprecation").and("-Xfatal-warnings")),
compileFilesInDir("tests/neg-custom-args/isInstanceOf", allowDeepSubtypes and "-Xfatal-warnings"),
Expand All @@ -182,7 +180,6 @@ class CompilationTests {
compileFile("tests/neg-custom-args/matchable.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
compileFile("tests/neg-custom-args/i7314.scala", defaultOptions.and("-Xfatal-warnings", "-source", "future")),
compileFile("tests/neg-custom-args/capt-wf.scala", defaultOptions.and("-language:experimental.captureChecking", "-Xfatal-warnings")),
compileDir("tests/neg-custom-args/hidden-type-errors", defaultOptions.and("-explain")),
compileFile("tests/neg-custom-args/i13026.scala", defaultOptions.and("-print-lines")),
compileFile("tests/neg-custom-args/i13838.scala", defaultOptions.and("-Ximplicit-search-limit", "1000")),
compileFile("tests/neg-custom-args/jdk-9-app.scala", defaultOptions.and("-release:8")),
Expand Down
19 changes: 19 additions & 0 deletions library/src/scala/quoted/Quotes.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package scala.quoted

import scala.annotation.experimental
import scala.annotation.implicitNotFound
import scala.reflect.TypeTest

/** Current Quotes in scope
Expand All @@ -21,7 +22,25 @@ transparent inline def quotes(using q: Quotes): q.type = q
*
* It contains the low-level Typed AST API metaprogramming API.
* This API does not have the static type guarantees that `Expr` and `Type` provide.
* `Quotes` are generated from an enclosing `${ ... }` or `scala.staging.run`. For example:
* ```scala sc:nocompile
* import scala.quoted._
* inline def myMacro: Expr[T] =
* ${ /* (quotes: Quotes) ?=> */ myExpr }
* def myExpr(using Quotes): Expr[T] =
* '{ f(${ /* (quotes: Quotes) ?=> */ myOtherExpr }) }
* }
* def myOtherExpr(using Quotes): Expr[U] = '{ ... }
* ```
*/

@implicitNotFound("""explain=Maybe this method is missing a `(using Quotes)` parameter.
Maybe that splice `$ { ... }` is missing?
Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call).
A splice can be thought as a method with the following signature.
def $[T](body: Quotes ?=> Expr[T]): T
""")
trait Quotes { self: runtime.QuoteUnpickler & runtime.QuoteMatching =>

// Extension methods for `Expr[T]`
Expand Down
2 changes: 2 additions & 0 deletions library/src/scala/util/boundary.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package scala.util
import scala.annotation.implicitNotFound

/** A boundary that can be exited by `break` calls.
* `boundary` and `break` represent a unified and superior alternative for the
Expand Down Expand Up @@ -34,6 +35,7 @@ object boundary:

/** Labels are targets indicating which boundary will be exited by a `break`.
*/
@implicitNotFound("explain=A Label is generated from an enclosing `scala.util.boundary` call.\nMaybe that boundary is missing?")
final class Label[-T]

/** Abort current computation and instead return `value` as the value of
Expand Down
23 changes: 23 additions & 0 deletions tests/neg-custom-args/explain/hidden-type-errors.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- [E007] Type Mismatch Error: tests/neg-custom-args/explain/hidden-type-errors/Test.scala:6:24 ------------------------
6 | val x = X.doSomething("XXX") // error
| ^^^^^^^^^^^^^^^^^^^^
| Found: String
| Required: Int
|---------------------------------------------------------------------------------------------------------------------
| Explanation (enabled by `-explain`)
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
| Tree: t12717.A.bar("XXX")
| I tried to show that
| String
| conforms to
| Int
| but the comparison trace ended with `false`:
|
| ==> String <: Int
| ==> String <: Int
| <== String <: Int = false
| <== String <: Int = false
|
| The tests were made under the empty constraint
---------------------------------------------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:33 ------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:33 ----------------------------------------
11 | var h = new HKT3_1[FunctorImpl](); // error // error
| ^
| Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any
Expand Down Expand Up @@ -26,7 +26,7 @@
|
| The tests were made under the empty constraint
--------------------------------------------------------------------------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/i11637.scala:11:21 ------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i11637.scala:11:21 ----------------------------------------
11 | var h = new HKT3_1[FunctorImpl](); // error // error
| ^
| Type argument test2.FunctorImpl does not conform to upper bound [Generic2[T <: String] <: Set[T]] =>> Any
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:3:27 -------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:3:27 -----------------------------------------
3 | def bar[T]: Unit = foo[T & Any] // error
| ^
| Type argument T & Any does not conform to lower bound Any
Expand All @@ -18,7 +18,7 @@
|
| The tests were made under the empty constraint
---------------------------------------------------------------------------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/i15575.scala:7:14 -------------------------------------------------
-- [E057] Type Mismatch Error: tests/neg-custom-args/explain/i15575.scala:7:14 -----------------------------------------
7 | val _ = foo[String] // error
| ^
| Type argument String does not conform to lower bound CharSequence
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-- [E042] Type Error: tests/neg-custom-args/i16601a.scala:1:27 ---------------------------------------------------------
-- [E042] Type Error: tests/neg-custom-args/explain/i16601a.scala:1:27 -------------------------------------------------
1 |@main def Test: Unit = new concurrent.ExecutionContext // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
| ExecutionContext is a trait; it cannot be instantiated
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions tests/neg-custom-args/explain/i16888.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- [E172] Type Error: tests/neg-custom-args/explain/i16888.scala:1:38 --------------------------------------------------
1 |def test = summon[scala.quoted.Quotes] // error
| ^
| No given instance of type quoted.Quotes was found for parameter x of method summon in object Predef
|---------------------------------------------------------------------------------------------------------------------
| Explanation (enabled by `-explain`)
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
| Maybe this method is missing a `(using Quotes)` parameter.
|
| Maybe that splice `$ { ... }` is missing?
| Given instances of `Quotes` are generated from an enclosing splice `$ { ... }` (or `scala.staging.run` call).
| A splice can be thought as a method with the following signature.
| def $[T](body: Quotes ?=> Expr[T]): T
---------------------------------------------------------------------------------------------------------------------
1 change: 1 addition & 0 deletions tests/neg-custom-args/explain/i16888.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test = summon[scala.quoted.Quotes] // error
Loading

0 comments on commit fd91ce1

Please sign in to comment.