-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Unsound use of ClassTag.unapply #7554
Comments
@sjrd said in this comment
|
Small fix: new TypeTest[p.T] {
def isInstance(x: Any): TypeTest.Result[x.type & p.T] = x match {
case x: (p.T & x.type) => TypeTest.success(x)
case _ => TypeTest.failure
}
} |
There is still something missing, when we do have an implicit object Test {
def main(args: Array[String]): Unit = {
val p1: T = T1
import p1.given
val p2: T = T1
import p2.given
(p1.y: p1.X) match {
case x: p2.Y => // should be an unchecked cast but is not
case x: p1.Y =>
case _ =>
}
}
}
trait T {
type X
type Y <: X
def x: X
def y: Y
given TypeTest[Y] = typeTestOfY
protected def typeTestOfY: TypeTest[Y]
}
object T1 extends T {
type X = Boolean
type Y = true
def x: X = false
def y: Y = true
protected def typeTestOfY: TypeTest[Y] = new {
def isInstance(x: Any): TypeTest.Result[x.type & Y] = x match
case x: (true & x.type) => TypeTest.success(x)
case _ => TypeTest.failure
}
} This can be solved by adding adding a second type parameter like in #7394. trait TypeTest[S, T <: S] {
def isInstance(x: S): TypeTest.Result[x.type & T]
def unapply(x: S): Option[x.type & T] =
TypeTest.unapply(x)(given this)
}
object TypeTest {
opaque type Result[A] = Boolean
def success[A](x: A): Result[A] = true
def failure[A]: Result[A] = false
def unapply[S, T <: S](x: S)(given tt: TypeTest[S, T]): Option[x.type & T] =
if tt.isInstance(x) then Some(x.asInstanceOf[x.type & T])
else None
} And using it in the following way trait T {
type X
type Y <: X
def x: X
def y: Y
given TypeTest[X, Y] = typeTestOfY
protected def typeTestOfY: TypeTest[X, Y]
}
object T1 extends T {
type X = Boolean
type Y = true
def x: X = false
def y: Y = true
protected def typeTestOfY: TypeTest[X, Y] = new {
def isInstance(x: X): TypeTest.Result[x.type & Y] = x match
case x: (true & x.type) => TypeTest.success(x)
case _ => TypeTest.failure
}
} Though we would need to ensure that |
We could also consider defining it like trait TypeTest[S, T <: S] {
def unapply(x: S): TypeTest.Result[x.type & T]
}
object TypeTest {
opaque type Result[A] = A | Failure.type
object Result {
given [A](res: Result[A]) { // currently fails, not sure where to put the extension methods
def isEmpty: Boolean = res == Failure
def get: A = res.asInstanceOf[A]
}
}
private object Failure
def success[A](x: A): Result[A] = x
def failure[A]: Result[A] = Failure
} |
In the links from my gist, there was also another proposal: scala/bug#9565 (comment). IIRC it was sound, so maybe somebody should compare. Not that I see anything wrong in @sjrd proposal, but I haven't studied it. Fwiw, re the "Small fix" in #7554 (comment), I thought the original code was supposed to compile: the extra Singleton should be inferred, and that's central to @AleksanderBG's GADT work, even tho that wasn't part of the spec. Isn't that settled by now? |
A less ad-hoc alternative to the opaque type approach that also does not allocate would be to define an abstract base class of Then, we can define: trait TypeTest[A] {
def isInstance(x: Any): x.type <?< A
} And GADT pattern matching (ping @AleksanderBG) allows us to recover the Implementing new TypeTest[p.T] {
def isInstance(x: Any): x.type <?< p.T = x match {
case x: (p.T & x.type) => implicitly
case _ => Not_<:<
}
} Maybe we'll need to help the compiler and write |
It's settled, but I've never gotten around to actually implementing it in the compiler. So far it's necessary to manually add the singleton type to the pattern.
That won't work, GADT patmat cannot currently constrain singleton types. A mechanism for constraining singleton types is already implemented for explicit null types in their PR, so after that gets merged, we could look into reusing it for GADTs. |
The least ad-hoc alternative and most straightforward version so far is trait TypeTest[-S, T] {
/** A TypeTest[S, T] can serve as an extractor that matches only S of type T.
*
* The compiler tries to turn unchecked type tests in pattern matches into checked ones
* by wrapping a `(_: T)` type pattern as `tt(_: T)`, where `ct` is the `TypeTest[S, T]` instance.
* Type tests necessary before calling other extractors are treated similarly.
* `SomeExtractor(...)` is turned into `tt(SomeExtractor(...))` if `T` in `SomeExtractor.unapply(x: T)`
* is uncheckable, but we have an instance of `TypeTest[S, T]`.
*/
def unapply(x: S): Option[x.type & T]
} The semantics are trivial as no extra concept must be added to encode the result. It also seamlessly works as The synthesized version would be new TypeTest[S, T] {
def unapply(x: S): Option[x.type & T] = x match {
case x: T => Some(x)
case _ => None
}
} which can be synthesised as a SAM ((x: S) => x match { case x: T => Some(x); case _ => None }): TypeTest[S, T] |
There are some cases where we must provide a type test but by providing a sound type test we may provide a |
I agree that case x: T where I think we still need to issue the class-tag based test since otherwise we would change runtime behavior wrt Scala 2. But we should add an unchecked warning that this will not work for types that are not simple class types. And then we need a type-safe way to bring back the functionality. I have been playing with a variant of shapeless Typeable in #9840. That seems to do the trick. Importantly, it does this without requiring any extension to the language. |
Note that the shapeless Typable fixes only some of the unsoundness that comes from having it encoded inside The second important contribution of the tree match
// Using TypeTest
+ case tree @ If(cond: Literal, thenp, elsep) =>
// using old Refect with explicit casting extractors
- case IsIf(tree @ If(IsLiteral(cond), thenp, elsep)) =>
// using Typable
- case Typeable.instanceOf[If](tree @ If(Typeable.instanceOf[Literal](cond), thenp, elsep)) => This example reflects only a simple case that is still short enough for one line, but when we used nested pattern things get way out of hand. This kind of pattern proved to be extremely hard to reason about and to learn to use properly. Using
|
I forgot to mention that the explicit casting extractors also avoided the unsoundness present with the |
Maybe @milessabin could weigh in on the soundness issue. // Using TypeTest
How is that obtained? Is that just an unapply in EDIT: Looking at #7555, it's done with compiler magic. That's problematic. It's one thing to propose a complicated and hard to explain trait like Maybe the question we should ask first is: What are general improvements to extractors that would allow us to make this work with convenient syntax? I believe the general understanding and implementation of extractors is lacking. I have stated many times that it would be very worthwhile for someone to really understand pattern matching typing and be able to come up with a spec and possible improvements. The previous attempts at this fell short. So currently the situation is that, because |
implicit def IfClassTag: ClassTag[If] = ....
given IfTypeTest as TypeTest[Tree, If] = ...
object If:
def unapply(tree: If): Some[(Tree, Tree, Tree)] = Some((tree.cond, tree.thenp, tree.elsep)) As the unapply takes an tree match
case tree @ If(cond, thenp, elsep) =>
// case IfClassTag(tree @ If(cond, thenp, elsep)) =>
// case IfTypeTest(tree @ If(cond, thenp, elsep)) =>
case tree: If =>
// case IfClassTag(tree) =>
// case IfTypeTest(tree) =>
The stricter argument type is what allows us to soundly state that we cannot check check at runtime that The proposed generalization in #9843 is otrtogonal to the enhancements made by ThThe definition of |
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
Fix #7554: Add TypeTest for sound pattern type test
`TypeTest` is a replacemnt for the same functionallity performed by the `ClassTag.unaplly`. Using `ClassTag` instances happend to be unsound. `TypeTest` fixes that unsoundess and adds extra flexibility with the `S` type. `ClassTag` type tests will still be supported but a warining will be emitted after 3.0.
Issue
This unfortunately is known to break when used with path dependent type.
Example
The following example shows a couple of cases where it fails
The text was updated successfully, but these errors were encountered: