Skip to content

The singleton instance trick used by =:= is unsound #8430

Open
@smarter

Description

@smarter

In the standard library, =:= is defined as follow:

  // the only instance for <:< and =:=, used to avoid overhead
  private val singleton: =:=[Any, Any] = new =:=[Any,Any] {
    // ...
  }
  implicit def refl[A]: A =:= A = singleton.asInstanceOf[A =:= A]
}

I always thought this was perfectly fine, but thanks to Dotty's world-class GADT support, it isn't anymore :)

object Test {
  def cast[A, B](elem: A, default: B)(implicit x: A =:= A, y: B =:= B): B = x match {
    case x: y.type =>
      elem
    case _ =>
      default
  }

  def main(args: Array[String]): Unit = {
    val x: String = cast(1, "") // ClassCastException
  }
}

The same code also compiles and crashes in scalac, but produces an unchecked erasure warning which I believe is incorrect: if it weren't for the cast used to get an instance of =:=, this code would be perfectly fine.

What can we do about this? In the short-term, either do nothing or special-case the GADT handling of =:= in the compiler. in the long-term, we should rewrite =:= and <:< using an opaque type to some dummy value:

opaque type <:<[-From, +To] = Object
opaque type =:=[From, To] <: <:<[From, To] = Object
private val singleton: =:=[Any, Any] = new Object
implicit def refl[A]: A =:= A = singleton
// ... and a bunch of extension methods on <:< and =:=

There's still a cast (EDIT: actually not needed), but the use of opaque types prevents the bogus inference of GADT bounds:
https://github.com/lampepfl/dotty/blob/b448db2454e003f1448a31746daed11001a2ec86/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L869-L870

(another solution would be to somehow enforce that values of type =:= must always be erased at runtime, and therefore do not have a notion of reference equality)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions