Should we actively avoid compile-time error being shadowed by implicit not found error?

This is a continuation of a discussion with @eed3si9n on twitter, regarding the case when a developer wants to deliberately cause the compilation to fail early with a custom message:

(Using Scala 2.13 as an example but you all know how to do it in Scala 3)

class AlwaysError {}

object AlwaysError {

  implicit def summon: AlwaysError = macro Macros.trigger

  class Macros(val c: whitebox.Context) {

    def trigger: c.Tree = {
      c.abort(c.enclosingPosition, "explicit error message!")
      ???
    }
  }
}

It can be assumed that “explicit error message!” should be seen by its user whenever the function summon is called. But in reality, it may be hidden by other messages that are less relevant:

  AlwaysError.summon //: error: explicit error message!

  implicitly[AlwaysError] //: error: explicit error message!

  trait IndirectError

  object IndirectError {

    implicit def summon(
        implicit
        ev: AlwaysError
    ): IndirectError = ???
  }

  implicitly[IndirectError] //: error: implicit error;

In the last case, the original message was hidden because it caused an ImplicitNotFound error in its parent context. Is it a bad design, given that “explicit error message!” comprises the most specific information?

Switching to Scala 3 or enabling “-VImplicits” won’t make it visible either. I haven’t tested the scenario on Scala 3, but to my best knowledge, user-defined compile-time error message won’t be given special treatment over general ImplicitNotFound message.

The only mitigation (more like a circumvention) I’m aware of is to overwrite the @ImplicitNotFound annotation of the top-level type symbol. This technique is used for error reporting of Lazy in shapeless, and that of Op in singleton-ops, but it may not begithu unreliable (see this bug for an example)

I was trying to implement a bypass in splain plugin, but turns out that the root of this problem is buried much deeper into the compiler than I expected. Please let me know if this is a feature/improvement that worth pursuing.

In Scala 3 use compiletime.error and transparent inline and you will only see the message you want.

2 Likes

It would make sense to at least have the option of displaying underlying errors, without printing the whole implicit resolution trace. E.g. with -verbose, or -explain which already exists in Scala 3.

1 Like

Thanks a lot, working in scala 3.2.x!

package com.tribbloids.spike.dotty

object InlineError {

  trait AlwaysError

  object AlwaysError {

    transparent inline given get: AlwaysError = {
      compiletime.error("error message!")
    }
  }

  trait Indirect1

  object Indirect1 {

    given get(
               using
               ev: AlwaysError
             ): Indirect1 = ???
  }

  trait Indirect2

  object Indirect2 {

    given get(
               using
               ev: Indirect1
             ): Indirect2 = ???
  }

  class Test1 {

    AlwaysError.get // error message!

    summon[Indirect1] //error message!
    summon[Indirect2] //error message!
  }
}

There is one caveat tho, this mechanism will throw the error IMMEDIATELY, disregarding all the low-level assumptions given. E.g. in the following example:

package com.tribbloids.spike.dotty

object InlineError {

  trait AlwaysError

  object AlwaysError {

    transparent inline given get: AlwaysError = {
      compiletime.error("error message!")
    }
  }

  trait Indirect1

  trait Test2_Imp0 {

    given get0: Indirect1 = ???

  }

  object Test2  extends Test2_Imp0 {

    given get2(
               using
               ev: AlwaysError
             ): Indirect1 = ???

    summon[Indirect1] //error message 2!
  }
}

The use case in question should get our compiler to try get2, encounter the inline error, then fallback to get0 of lower priority. And if and only if all fallback options have failed, it will fail with all the trial and error information on the path collected and reported.

But nooooo, once inline error is called it will fail fatally and abort the search.

Specifically, your singleton-ops has the OpIntercept PR hanging for a long time:

Once it is merged you’ll inevitably have multiple paths of evaluation, blocking this fallback mechanism won’t be in your favour

Agreed, at this moment If without transparent inline given, most of the error information won’t be specific enough even in Dotty:

  trait AlwaysError

  object AlwaysError {

    implicit def get(): AlwaysError = {
      compiletime.error("error message!")
    }
  }

  trait Indirect1

  object Indirect1 {

    given get(
               using
               ev: AlwaysError
             ): Indirect1 = ???
  }

  trait Indirect2

  object Indirect2 {

    given get(
               using
               ev: Indirect1
             ): Indirect2 = ???
  }

  object Test1 {

    AlwaysError.get() // no error

    summon[Indirect1]
    summon[Indirect2]
  }

The error messages are:

+++++++++++++++++++++++++++++++
[Error] /xxx/src/main/scala/com/tribbloids/spike/dotty/InlineError.scala:38:22: No given instance of type com.tribbloids.spike.dotty.InlineError.Indirect1 was found for parameter x of method summon in object Predef.
I found:

com.tribbloids.spike.dotty.InlineError.Indirect1.get(
  /* missing */summon[com.tribbloids.spike.dotty.InlineError.AlwaysError]
)

But no implicit values were found that match type com.tribbloids.spike.dotty.InlineError.AlwaysError.

The following import might make progress towards fixing the problem:

import com.tribbloids.spike.dotty.InlineError.Test2.get2

[Error] /xxx/src/main/scala/com/tribbloids/spike/dotty/InlineError.scala:39:22: No given instance of type com.tribbloids.spike.dotty.InlineError.Indirect2 was found for parameter x of method summon in object Predef.
I found:

com.tribbloids.spike.dotty.InlineError.Indirect2.get(
  com.tribbloids.spike.dotty.InlineError.Indirect1.get(
    /* missing */summon[com.tribbloids.spike.dotty.InlineError.AlwaysError]
  )
)

But no implicit values were found that match type com.tribbloids.spike.dotty.InlineError.AlwaysError.
+++++++++++++++++++++++++++++++++

I’m afraid most of my understanding of Scala compiler (through writing for splain plugin) are obsolete in Dotty, so I certainly hope to have one your master student to propose a plugin or patch