Skip to content
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

Allow @implicitNotFound messages as explanations #16893

Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Feb 12, 2023

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?
   ---------------------------------------------------------------------------------------------------------------------

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?
   ---------------------------------------------------------------------------------------------------------------------
```
@odersky
Copy link
Contributor Author

odersky commented Feb 12, 2023

@nicolasstucki We can use an analogous scheme to improve the situation for #16888.

@prolativ
Copy link
Contributor

Should we somehow document this new behaviour or is it supposed to be used only internally?

@nicolasstucki
Copy link
Contributor

Unfortunately, the spec of implicitNotFound states how the error will be shown. Changing this could break existing use cases.

A solution to this might be to show the custom message and hide the compiler-generated message under -explain.

The message A Label is generated from an enclosing `scala.util.boundary` call. Maybe that boundary is missing? does not seem to fit exactly the intended use of implicitNotFound. It feels like we should have a way to customize the message and the -explain. The example would be the custom -explain. We could modify the class implicitNotFound to take a message and explain string or add a separate @implicitNotFoundExplain.

Maybe this is how an error message should look

-- [E172] Type Error: tests/neg-custom-args/explain/labelNotFound.scala:2:30 -------------------------------------------
2 |  scala.util.boundary.break(1) // error
  |                              ^
  |  missing Int boundary label
  |---------------------------------------------------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | No given instance of type scala.util.boundary.Label[Int] was found for parameter label of method break in object boundary
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  | A Label[Int] is generated from an enclosing `scala.util.boundary` call.
  | Maybe that boundary is missing?
  | Maybe a given Label[Int] is missing from the parameters?
   ---------------------------------------------------------------------------------------------------------------------

Copy link
Contributor

@nicolasstucki nicolasstucki left a comment

Choose a reason for hiding this comment

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

We should not break the contract of implicitNotFound

@odersky
Copy link
Contributor Author

odersky commented Feb 14, 2023

Unfortunately, the spec of implicitNotFound states how the error will be shown. Changing this could break existing use cases.

In theory yes, in practice, I would be really surprised if someone started an implicitNotFound message with ....

@odersky
Copy link
Contributor Author

odersky commented Feb 14, 2023

But I guess it's OK to add an implicitNoFoundExplain annotation. Do you want to add that one? I just wanted to show what needs to be done, but won't have the time to do it.

@prolativ
Copy link
Contributor

Adding a new annotation would have to be done in a minor release, unless we make it experimental

@bishabosha
Copy link
Member

bishabosha commented Feb 16, 2023

Adding a new annotation would have to be done in a minor release, unless we make it experimental

the release candidates (e.g. 3.3.0-RC2) still have experimental tasty version, so its ok to release for 3.3.0?

@odersky
Copy link
Contributor Author

odersky commented Feb 16, 2023

So somebody should do it quickly! These are real and very important usability improvements. We should not block them by procedural reasons.

@prolativ
Copy link
Contributor

How would we like this new annotation to be used then? Should it be orthogonal to implicitNotFound, meaning that the old annotation would only replace the error message and the new one would only add explanation?

@odersky
Copy link
Contributor Author

odersky commented Feb 16, 2023

How would we like this new annotation to be used then? Should it be orthogonal to implicitNotFound, meaning that the old annotation would only replace the error message and the new one would only add explanation?

Yes, I think that's reasonable.

@smarter
Copy link
Member

smarter commented Feb 16, 2023

In Scala 2, @implicitNotFound does not replace the message, both are shown. This difference in behavior has lead to some confusion as described in #11493, and it seems people are happy with doing it the Scala 2 way which wouldn't require introducing a new annotation.

@odersky
Copy link
Contributor Author

odersky commented Feb 17, 2023

@smarter I did not know that Scala 2 behaved differently. Yes, let's go back to that then the whole discussion about explain becomes redundant.

@nicolasstucki
Copy link
Contributor

In Scala 2, @implicitNotFound does not replace the message, both are shown. This difference in behavior has lead to some confusion as described in #11493, and it seems people are happy with doing it the Scala 2 way which wouldn't require introducing a new annotation.

It does not seem to be the case. In Scala 2 only the custom message is shown. It seems that the behaviour is the same as in Scala 3.

@scala.annotation.implicitNotFound("Foo[${T}] was not found")
final class Foo[T]
class C {
  def test = implicitly[Foo[Int]]
}
~/GitHub/dotty @ main % scala-cli compile -S 2.13 Foo.scala
Compiling project (Scala 2.13.10, JVM)
[error] ./Foo.scala:4:14: Foo[Int] was not found
[error]   def test = implicitly[Foo[Int]]
[error]              ^^^^^^^^^^^^^^^^^^^^
Error compiling project (Scala 2.13.10, JVM)
Compilation failed
~/GitHub/dotty @ main % scala-cli compile -S 3.2 Foo.scala
Compiling project (Scala 3.2.2, JVM)
[error] ./Foo.scala:4:34: Foo[Int] was not found
[error]   def test = implicitly[Foo[Int]]
[error]                                  ^
Error compiling project (Scala 3.2.2, JVM)
Compilation failed

@prolativ
Copy link
Contributor

It looks like the behaviour in scala 2 depends on whether the annotation is inherited or not.

import annotation.implicitNotFound

@implicitNotFound("Missing type: Foo")
trait Foo

@implicitNotFound("Missing type: Bar")
trait Bar

trait Baz extends Bar

object Test {
  def myImplicitly[T](implicit @implicitNotFound("Missing parameter: ${T}") t: T): T = t

  val a = implicitly[Int]
  val b = implicitly[Foo]
  val c = implicitly[Baz]

  val x = myImplicitly[Int]
  val y = myImplicitly[Foo]
  val z = myImplicitly[Baz]
}

Compiling with scala 2:

scala-cli compile Test.scala -S 2.13.10
[error] ./Test.scala:14:11: could not find implicit value for parameter e: Int
[error]   val a = implicitly[Int]
[error]           ^^^^^^^^^^^^^^^
[error] ./Test.scala:15:11: Missing type: Foo
[error]   val b = implicitly[Foo]
[error]           ^^^^^^^^^^^^^^^
[error] ./Test.scala:16:11: could not find implicit value for parameter e: Baz (Missing type: Bar)
[error]   val c = implicitly[Baz]
[error]           ^^^^^^^^^^^^^^^
[error] ./Test.scala:18:11: Missing parameter: Int
[error]   val x = myImplicitly[Int]
[error]           ^^^^^^^^^^^^^^^^^
[error] ./Test.scala:19:11: Missing parameter: Foo
[error]   val y = myImplicitly[Foo]
[error]           ^^^^^^^^^^^^^^^^^
[error] ./Test.scala:20:11: Missing parameter: Baz
[error]   val z = myImplicitly[Baz]
[error]           ^^^^^^^^^^^^^^^^^

Compiling with scala 3:

scala-cli compile Test.scala -S 3.3.0-RC2
[error] ./Test.scala:14:26: No given instance of type Int was found for parameter e of method implicitly in object Predef
[error]   val a = implicitly[Int]
[error]                          ^
[error] ./Test.scala:15:26: Missing type: Foo
[error]   val b = implicitly[Foo]
[error]                          ^
[error] ./Test.scala:16:26: Missing type: Bar
[error]   val c = implicitly[Baz]
[error]                          ^
[error] ./Test.scala:18:28: Missing parameter: Int
[error]   val x = myImplicitly[Int]
[error]                            ^
[error] ./Test.scala:19:28: Missing parameter: Foo
[error]   val y = myImplicitly[Foo]
[error]                            ^
[error] ./Test.scala:20:28: Missing parameter: Baz
[error]   val z = myImplicitly[Baz]
[error]                            ^

@odersky
Copy link
Contributor Author

odersky commented Feb 17, 2023

I'd vote for showing both messages always. It seems the implicit not found messages are not always that specific, so adding info which implicit parameter was not found is useful.

@nicolasstucki
Copy link
Contributor

I'd vote for showing both messages always. It seems the implicit not found messages are not always that specific, so adding info which implicit parameter was not found is useful.

Sounds good. But we should put the custom error first.

@odersky
Copy link
Contributor Author

odersky commented Feb 17, 2023

Sounds good. But we should put the custom error first.

I am not sure about that. Can we do play out some scenarios how it would look?

For the missing label we'd get with custom error last:

No given instance of type scala.util.boundary.Label[Int] was found for parameter label of method break in object boundary

A Label is generated from an enclosing `scala.util.boundary` call.
Maybe that boundary is missing?`

With custom error first this makes no sense.

Another use case is the implicitNotFound for <:< Here we would have with custom error first:

Cannot prove that Int <:< String.

No given instance of type <:<[Int, String] was found for parameter xs of method flatten.

Or, custom error last:

No given instance of type <:<[Int, String] was found for parameter xs of method flatten.

Cannot prove that Int <:< String.

Here it's debatable, but I think in the end I would also prefer custom error last.

@prolativ
Copy link
Contributor

I believe we have two kinds of use cases:

  1. The @implicitNotFound annotation is intended to hide the fact that some library uses implicits in it's API to make it more friendly to users who are not familiar with the concept of implicits, e.g.
package calendar

import annotation.implicitNotFound

trait DateFormat

object DateFormats {
  implicit object DayMonthYear extends DateFormat
  implicit object MonthDayYear extends DateFormat
}

class Date(day: Int, month: Int, year: Int) {
  def show(implicit @implicitNotFound("To show a date one needs to specify a date format, e.g. with `import calendar.DateFormats.DayMonthYear`") dateFormat: DateFormat): String = ???
}

In this situation it might make sense to:
a) show only the custom message by default
b) show the custom message followed by the generic No given instance... message and a link to the documentation describing what implicits/givens are, when -explain is enabled

  1. The message from @implicitNotFound is intended to give some clue (to slightly more advanced users) why exactly a given instance was not found - in this case the custom message seems to make more sense when shown after the generic message

The problem is that it's not so easy to tell which situation we're dealing with in general

@prolativ
Copy link
Contributor

In general it seems like @implicitNotFound is slightly controversial, e.g. typelevel/cats#4060

@odersky
Copy link
Contributor Author

odersky commented Feb 20, 2023

I believe the cats example shows that we want additional info, not a replacement of the error message. E.g. see this comment:

I think this annotation is still useful, if you want to provide additional context (e.g., how to get that implicit). That seems to have been the original goal in typelevel/cats#581, although it was never executed (?) and isn't relevant anymore anyway. But just saying what is missing is something the compiler does anyway, and I think (hope) has been doing for a while? :)

@prolativ
Copy link
Contributor

Yeah, I would say cats classifies as case 2

@odersky
Copy link
Contributor Author

odersky commented Feb 20, 2023

And I would argue the DateFormat case also classifies as case 2, not 1. Taken alone, the implicitNotFound message is more confusing than helpful. If it is shown after the regular message it makes sense.

I think by now we have not seen a single case where custom first is clearly better and lots of cases where it makes not sense at all. Let's close this issue and implement it.

@Kordyjan Kordyjan assigned nicolasstucki and unassigned odersky Feb 27, 2023
This approach mirrors the format used in `@noWarn`. There it is possible
to tag encode the category and message in the string.
```
@nowarn("cat=deprecation&msg=Implementation ...")
```

The current implementation for `explain=` only allows this tag at the
start of the string. Making the message either a old-style message or an
message in the explain.

We could extend this to support `msg=` to be able to have both a custom
message and an explain. Furthermore we could insert some flags to
explicitly state how to display the original message (for example:
not-found=hidden, not-found=in-message, not-found=in-explain).
@nicolasstucki
Copy link
Contributor

nicolasstucki commented Feb 28, 2023

I ended up keeping the current scheme for generating -explain messages but modified the format slightly to make it clearer and easily extensible. See 7de74bd.

I also added the implicit not found explain for Quotes.

@nicolasstucki nicolasstucki requested a review from sjrd March 2, 2023 19:13
@nicolasstucki nicolasstucki assigned sjrd and unassigned nicolasstucki Mar 2, 2023
@nicolasstucki
Copy link
Contributor

@sjrd could you review 7de74bd? The spec part of it.

@sjrd
Copy link
Member

sjrd commented Mar 3, 2023

What do you call "the spec part of it"? The commit message? I see no spec changes in the PR diff.

@nicolasstucki
Copy link
Contributor

I meant the addition of the explain= tag in the message and how we special case it.

@nicolasstucki
Copy link
Contributor

Can I create a copy of ImplicitNotFound into the dotty library and update the documentation? Not sure if that could cause conflicts when lading the compiled binaries.

Copy link
Contributor Author

@odersky odersky left a comment

Choose a reason for hiding this comment

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

Small typo, otherwise LGTM

@sjrd
Copy link
Member

sjrd commented Mar 7, 2023

I meant the addition of the explain= tag in the message and how we special case it.

That seems fine to me.

@nicolasstucki
Copy link
Contributor

I meant the addition of the explain= tag in the message and how we special case it.

That seems fine to me.

Good, then it is ready to be merged.

@nicolasstucki nicolasstucki merged commit fd91ce1 into scala:main Mar 9, 2023
@nicolasstucki nicolasstucki deleted the add-implicitnotfound-explain branch March 9, 2023 10:23
@Kordyjan Kordyjan added this to the 3.3.1 milestone Aug 1, 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.

7 participants