Pre-SIP: Custom compile-time type show for error messages

When using an ecosystem with complex type classes it is easy to get lost in the large type error messages. One method to mitigate it is using a compiler plugin like scala-clippy (which is not available for Scala 3).

This PR proposes a new approach with an addition of compiletime.hasCustomShow and compiletime.CustomShow by enabling library owners to define custom compiletime show strings for their classes. These strings will be fetched by scalac when running show if the class/type that is annotated with compiletime.hasCustomShow and has an available implicit set by compiletime.CustomShow.

Example:

import scala.compiletime.{CustomShow, hasCustomShow}

@hasCustomShow
trait Foo[T]
type Baz = Foo["To Infinity And Beyond!"]
object Foo:
  given CustomShow[Baz] with {
    type Out = "Baz"
  }

val x: Foo["To Infinity And Beyond!"] = 1 // error

Output:

 val x: Foo["To Infinity And Beyond!"] = 1 // error
   |                                          ^
   |                                          Found:    (1 : Int)
   |                                          Required: Baz

See draft PR here:

For discussion:

  1. Better/alternative approaches?
  2. Do we need also better mechanisms to customize error messages in general? Even if we manage to reduce the type signature complexity using CustomShow, maybe itā€™s good to also have a way to define an implicit like CustomError[ErrID, T], that will replace the specific error for type T or other filtering possibilities.
  3. Does this change require a SIP or not? Can be considered part of the dotty standard library under scala.compiletime.
1 Like

Could you give an example from a real situation, to see better how this helps ?

  1. Iā€™d like us to have a unified way to throw/customize compile-time errors for inline, match types, implicit not found, and this
  2. This seems like it does change the language, as it couldnā€™t be done in userland, so a SIP seems appropriate

In my library I can do something like (Bit, Bits[8]) <> VAR for values or types (using match type), which results with the type signature of

DFVal[DFTuple[(DFVal[Bit, Modifier[X,Y,Z,VAL]], DFVal[Bits[8], DFVal[Bit, Modifier[X,Y,Z,VAL]])], Modifier[X, Y, Z, VAR]] //The `X`, `Y` and `Z` tags can contain additional information.

This is crazy. I do not want to see the full signature, but just the syntax leading up to it: (Bit, Bits[8]) <> VAR.
BTW, the same goes for the Named Tuples SIP. Since Named Tuples are just composition of opaque types and tuples, then we can end up with a very complex type signature, instead of the condensed named tuple representation.

To date, everything added under scala.compiletime added something that cannot be done in userland, yet without any SIP.

Oh I see, that is definitely surprising !

Good point !

I think we should still show the non-pretty signature in some situations, maybe on request ?
Otherwise we might get situations like:

Expected: MyPrettyPrintedType[A, B] // Actually MyType[A, B, 1]
Found:    MyPrettyPrintedType[A, B] // Actually MyType[A, B, 2]

MUnit has a pretty reasonable default behavior here, and falls back to structural printing which I think is generated via reflection when the values are different but the rendered string representation is the same.

Itā€™s occasionally horrifically ugly (itā€™s particularly bad at Chain), but itā€™s much better than having to deal with a test failing with seemingly identical expected and actual values.

Having to supply both isnā€™t very ergonomic. Is there any problem with simply looking for the given? Obviously if someone tried to actively use it to be misleading (by writing their own given somewhere else) it would be a mess, but weā€™re not anticipating an adversarial situation here, right?

This is to limit redundant implicit lookup upon show. It should be quite rare to have custom type show, so it does not make sense to always look it up.

Is show itself used so frequently that this is likely to be a non-negligible part of compilation time?

It is used on every hover, completions, etc via LSP. But otherwise only in error messages

I like the spirit of this proposal.

Another tangential use-case where a better representation for types would be useful is this use-case . It would be nice if we could include that use-case within this design.

1 Like