Description
A new variant of #50 (re)surfacing, haven't checked yet among all the original examples, nor if this is fixed by the existing PRs (in particular, #4036). The issue is that (({ type R = A }) & ({ type R = B }))#R
is allowed and shouldn't, so I adapted @alexknvl's code in #4060 to implement coerce
.
object App {
def coerce[U, V](u: U): V = {
type X = { type R >: U }
type Y = { type R = V }
type Z = X & Y
val u1: Z#R = u
u1
}
def main(args: Array[String]): Unit = {
val x: Int = coerce[String, Int]("a")
println(x + 1)
}
}
$ dotc -d out UnsoundProj.scala
$ dotr -classpath out App
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
at App$.main(UnsoundProj.scala:11)
at App.main(UnsoundProj.scala)
This was prompted by a question from @allanrenucci. He also pointed me to the documentation on type projections (http://dotty.epfl.ch/docs/reference/dropped/type-projection.html), which seems both unclear and misleading:
Scala so far allowed general type projection T#A where T is an arbitrary type and A names a type member of T.
Dotty disallows this if T is an abstract type (class types and type aliases are fine). This change was made because unrestricted type projection is unsound.
Projections should be illegal on lots of types T
that aren't abstract types themselves — T
must be a class type (or alias to such), and A
should be a fully defined type member, but type operators like intersections, unions, structural refinements should be forbidden — probably all operators should (unless we have a very good reason for allowing some).
This way, type projections can only access global (type) constants:
class Defining {
type TypeMember = ...
}
object Defining {
val valueMember
}
// Here, Defining#T and Defining.valueMember are both references to known global constants, at value or type-level