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

Conversation

prolativ
Copy link
Contributor

@prolativ prolativ commented Oct 7, 2020

Porting the feature from Scala 2 with a slight improvement on handling nested parameterized types.

Partially based on #6703

Copy link
Member

@dottybot dottybot left a 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! ☀️

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch from d95fff7 to 2ae6999 Compare October 7, 2020 09:38
val paramsNames = ap.typeSymbol.typeParams.map(_.name.unexpandedName.toString)
val paramsValues = ap.widenExpr.dropDependentRefinement.argInfos
paramsNames.zip(paramsValues) ++ extractTypeParams(tycon)
case RefinedType(parent, _, refinedInfo) => extractTypeParams(parent) ++ extractTypeParams(refinedInfo)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any test case for refinement types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While trying to test that deeper I realized that there are potentially some more cases but I'm not sure how some of them should be handled. E.g. the piece of code below

@annotation.implicitNotFound("There's no Foo[${A}]") trait Foo[A]
implicitly[Foo]
  • when compiled with scala 2.13 simply returns the error
error: trait Foo takes type parameters
  • when compiled with dotty from latest master, throws an exception inside the compiler and kills the REPL
  • when compiled from this branch, prints the user's custom error message but substituting an empty string for the variable
There's no Foo[]

Should the behaviour in such a case be exactly the same as for scala 2.13? This is not clear for me because if no message is defined by the user, the behaviour is not the same. E.g. for this code

trait Bar[A]
implicitly[Bar]

the results are as follows:

  • scala 2.13:
error: trait Bar takes type parameters
  • dotty's latest master and this PR:
no implicit argument of type Bar was found for parameter ev of method implicitly in object DottyPredef

Should the same error be reported as in scala 2.13 then or does implicitly[Foo] without a type parameter for Foo actually make sense in dotty (I assume by treating a type constructor as a type lambda)? If yes then how should the type variable in Foo be substituted while printing the message?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smarter any thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implicitly takes a type parameter T upper-bounded by Any, so type constructors like Foo are not valid arguments for that method (in Dotty we can use an upper-bound of AnyKind to allow that sort of things), so Scala 2.13 is correct to complain (and in fact Dotty also complains if I try to write identity[List](???), it's only when the parameter list is implicit that it does something weird). So we should open an issue to fix Dotty's behavior here but this can be done independently from this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue reported, missing tests added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smarter Is it an expected behaviour that a method defined inside a refinement does not have a symbol assigned to it? A comment in Denotations.scala claims that

   *  A denotation might refer to `NoSymbol`. This is the case if the denotation
   *  was produced from a disjunction of two denotations with different symbols
   *  and there was no common symbol in a superclass that could substitute for
   *  both symbols.

but it's not clear for me if this should be the only case.

E.g. in the code below

trait Foo[A]

type Fooable[A] = {
  def foo(implicit @annotation.implicitNotFound("There's no Foo[${A}]") ev: Foo[A]): Any
}

val x: Fooable[Long] = null
x.foo

the tree corresponding to x.foo has no symbol so the solution using tree.symbol.paramSymss to extract the annotation on an argument from the method's definition doesn't work.

When the code above is compiled with scala 2.13, the error message gets resolved properly but the variable is not substituted

cmd3.sc:1: There's no Foo[]
val res3 = x.foo
             ^
Compilation Failed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an expected behaviour that a method defined inside a refinement does not have a symbol assigned to it?

Yes, the comment in Denotations is incomplete, structural members (members of refinement types) don't have a symbol (a symbol is supposed to have an owner, but a structural member doesn't have a parent which could be used as an owner, and we can't create a new symbol with a dummy owner every time we see a structural member, because two types with the same refinements are equal so it doesn't make sense that they would have members with different symbols)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the simplest solution is to disallow implicitNotFound annotations on structural members.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of compatibility with scala 2.13 then: should we raise an error in such a case or rather simply ignore the annotation and raise a warning?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning is fine.

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch 2 times, most recently from 869b03c to 6c45f5c Compare October 21, 2020 11:42
@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch 2 times, most recently from 2b541fe to dec6e61 Compare October 27, 2020 12:58

def msg(shortForm: String)(headline: String = shortForm) = arg match {
def missingArgMsg(arg: Tree, pt: Type, where: String, paramSymWithMethodTree: Option[(Symbol, Tree)] = None)(using Context): String = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is getting quite complex with many arguments, I think it's worth adding a documentation comment explaining what this does and what each parameter is for.

msg(userDefined.getOrElse(
em"no implicit argument of type $pt was found${location("for")}"))() ++
hiddenImplicitsAddendum
val userDefinedParamMessage = paramSymWithMethodTree.flatMap{ (sym, tree) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would add a comment with an example of an implicitNotFound on a parameter so the reader knows what we're looking for.

}
}

val userDefinedTypeMessage = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map{ rawMsg =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I would add a comment with an example of an implicitNotFound on a type definition

)
}

val shortMessage = userDefinedParamMessage.orElse(userDefinedTypeMessage).getOrElse(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we either use the userDefinedParamMessage or the userDefinedTypeMessage but never both, so they could be lazy vals or defs instead of vals.

wtp.paramNames.lazyZip(wtp.paramInfos).lazyZip(args).foreach { (paramName, formal, arg) =>
arg.tpe match {
case failure: SearchFailureType =>
val paramSymWithMethodTree = if tree.symbol.exists then Some((paramSyms(paramName), tree)) else None

report.error(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In many situation we will recover from an error without ever showing it to the user (e.g., in implicit search we might find another candidate), so it's important to delay computing anything we want to show to the user until we know we need it. Here things are a bit subtle but missingArgMsg returns a String and report.error takes as input a Message, this works because there is an implicit conversion from String to Message: https://github.com/lampepfl/dotty/blob/22c23a5cc648650bb6380dec0c11a5fdc34e1d73/compiler/src/dotty/tools/dotc/reporting/Message.scala#L15
Note that this conversion takes a => String as input and which means the call to missingArgMsg will be delayed, which is what we want. But it would be even better if paramSyms and paramSymWithMethodTree were also delayed, which can be achieved by computing them inside missingArgMsg (or making them both lazy vals)

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch from dec6e61 to 8cea91a Compare October 27, 2020 14:52
Comment on lines 938 to 939
/**
* Extracting the message from a method parameter, e.g. in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation style in the compiler is to start at the first line, and subsequent lines start with two spaces after the * to align the message:

        /** Extracting the message from a method parameter, e.g. in
         *  ....
         *  ....
         */

Comment on lines 945 to 946
def userDefinedParamMessage = paramSymWithMethodTree.flatMap{ (sym, tree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map{ rawMsg =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space before braces (same in a few other places):

Suggested change
def userDefinedParamMessage = paramSymWithMethodTree.flatMap{ (sym, tree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map{ rawMsg =>
def userDefinedParamMessage = paramSymWithMethodTree.flatMap { (sym, tree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>

Comment on lines 3118 to 3119
lazy val paramSyms = tree.symbol.paramSymss.flatten.map(sym => sym.name -> sym).toMap
def paramSymWithMethodTree(paramName: TermName) = if tree.symbol.exists then Some((paramSyms(paramName), tree)) else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paramSyms could be a non-lazy local val defined inside paramSymWithMethodTree now.

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch from 8cea91a to 6052353 Compare October 27, 2020 15:33
@prolativ
Copy link
Contributor Author

@smarter reworks done

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stuff is surprisingly tricky to get right but I think we're getting there slowly!


/** Extracting the message from a type, e.g. in
*
* implicit @annotation.implicitNotFound
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incomplete line?

*/
def userDefinedParamMessage = paramSymWithMethodTree.flatMap { (sym, tree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val paramSyms = typeVariablesInScope(sym)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"type variables" in the compiler usually refer the variables the compiler internally creates when performing type inference so it's better to not use that term, typeParamsInScope would be fine, and since this method is only used once you could inline it here actually: val typeParamsInScope = ...

*
* trait Foo
*
* def foo(implicit @annotation.implicitNotFound foo: Foo)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume this should be@implicitNotFound("some message")

def userDefinedParamMessage = paramSymWithMethodTree.flatMap { (sym, tree) =>
userDefinedMsg(sym, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val paramSyms = typeVariablesInScope(sym)
val paramTypeRefs = paramSyms.map(_.typeRef.typeConstructor)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the use of typeConstructor here is needed.

val methodTypeParams = tree.symbol.paramSymss.flatten.filter(_.isType)
val methodTypeArgs = targs.map(_.tpe)
paramTypeRefs.map(_.asSeenFrom(methodOwnerType, methodOwner).subst(methodTypeParams, methodTypeArgs))
case _ => paramTypeRefs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asSeenFrom is also needed when the tree isn't a polymorphic method:

import scala.annotation.implicitNotFound

class Foo[A] {
  def foo(implicit @implicitNotFound("A = ${A}") x: String): Unit = ???
}

object Test {
  val x: Foo[Int] = ???
  x.foo
}

Currently outputs A = A instead of A = Int

tpe match {
case ap @ AppliedType(tycon, _) =>
val paramsNames = ap.typeSymbol.typeParams.map(_.name.unexpandedName.toString)
val paramsValues = ap.widenExpr.dropDependentRefinement.argInfos
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think widenExpr.dropDependentRefinement is useful here.

* def foo(foo: Foo)
*/
def userDefinedTypeMessage = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map { rawMsg =>
def extractTypeParams(tpe: Type): List[(String, Type)] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this was broken before so doesn't really need to be addressed in this PR, but just looking at the type parameters of the type isn't enough, e.g.:

import scala.annotation.implicitNotFound

class Outer[A] {
  @implicitNotFound("A = ${A} -- B = ${B}")
  class Inner[B]
}

object Test {
  val x: Outer[Int] = new Outer[Int]
  implicitly[x.Inner[Double]]
}

I'd expect A = Int -- B = Double but instead I get A = -- B = Double, Scala 2 has the same issue! I think a solution based on asSeenFrom like what is done in userDefinedParamMessage would make more sense, but I haven't tried it.

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch from 6052353 to 8fc8aaa Compare October 29, 2020 19:19
@prolativ
Copy link
Contributor Author

@smarter reworks done. Using asSeenFrom in both cases indeed helped and the code got slightly simpler. While debugging the use cases that you found out not to work, I semiaccidentally removed the restriction that in the messages one could refer to type parameters but not to type members. I also added some test cases. The only thing I'm not sure is where to put the substitutableTypeSymbolsInScope method which I use in Implicits.scala and RefChecks.scala

@smarter
Copy link
Member

smarter commented Oct 29, 2020

Nice!

The only thing I'm not sure is where to put the substitutableTypeSymbolsInScope method which I use in Implicits.scala and RefChecks.scala

Since it's only used in error messages, perhaps in ErrorReporting.scala ? In fact, maybe other parts of the code like missingArgMsg could be moved to that file too.

@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch 2 times, most recently from 2c8dedd to 391719d Compare November 3, 2020 09:48
@prolativ
Copy link
Contributor Author

prolativ commented Nov 3, 2020

@smarter refactor done and tests fixed

@smarter
Copy link
Member

smarter commented Nov 3, 2020

test performance please

@dottybot
Copy link
Member

dottybot commented Nov 3, 2020

performance test scheduled: 1 job(s) in queue, 1 running.

@dottybot
Copy link
Member

dottybot commented Nov 3, 2020

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/9957/ to see the changes.

Benchmarks is based on merging with master (7eac235)

Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise LGTM!

Comment on lines 319 to 323
val methodOwnerType: Type = applTree match {
case Select(qual, _) => qual.tpe
case TypeApply(Select(qual, _), _) => qual.tpe
case _ => methodOwner.typeRef
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use tpd.decomposeCall to extract the method that is applied (as well as the type arguments which are used below), and then tpd.qualifier to get the prefix.

* def foo(implicit foo: Foo): Any = ???
*/
private def userDefinedImplicitNotFoundTypeMessage = userDefinedMsg(pt.typeSymbol, defn.ImplicitNotFoundAnnot).map { rawMsg =>
val substituteType = (_: Type).asSeenFrom(pt, pt.typeSymbol)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument of asSeenFrom is supposed to be a class so pt.classSymbol is more appropriate (if pt is a class type it's the same as typeSymbol, it'll be different for a type alias for example).

Also fixes some complex cases of resolving type variables in `implicitNotFound` message for annotations put on type definitions
@prolativ prolativ force-pushed the implicit-not-found-on-parameters branch from 391719d to 1760da3 Compare November 4, 2020 08:52
@prolativ
Copy link
Contributor Author

prolativ commented Nov 4, 2020

@smarted reworks done. Is there a way to rerun only a part of the CI workflow? The normal tests seem not to have even started properly but all the other parts succeeded

@smarter
Copy link
Member

smarter commented Nov 4, 2020

If the tests haven't started it means all our runners are busy running other jobs, the only thing to do is wait.

@prolativ
Copy link
Contributor Author

prolativ commented Nov 4, 2020

Now test_bootstrapped randomly failed with java.lang.NoClassDefFoundError: Could not initialize class sbt.internal.parser.SbtParser$ although it succeeded before and no code was changed in between

@smarter
Copy link
Member

smarter commented Nov 4, 2020

Looks like the sbt cache was corrupted, I've deleted it on the server, let's merge since all the tests passed at least once.

@smarter smarter merged commit 5e8c13f into scala:master Nov 4, 2020
@prolativ prolativ deleted the implicit-not-found-on-parameters branch November 5, 2020 15:58
@Kordyjan Kordyjan added this to the 3.0.0 milestone Aug 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants