Skip to content

Commit

Permalink
Fix scala#7554: Implement TypeTest interface
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasstucki committed Sep 8, 2020
1 parent 904f407 commit 7f9d8dd
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 10 deletions.
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,10 @@ class Definitions {
@tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply)


@tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest")
@tu lazy val TypeTestModule: Symbol = TypeTestClass.companionModule
@tu lazy val TypeTestModule_identity: Symbol = TypeTestModule.requiredMethod(nme.identity)

@tu lazy val QuotedExprClass: ClassSymbol = requiredClass("scala.quoted.Expr")
@tu lazy val QuotedExprModule: Symbol = QuotedExprClass.companionModule

Expand Down
32 changes: 32 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,37 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
case _ => EmptyTree
end synthesizedClassTag

val synthesizedTypeTest: SpecialHandler =
(formal, span) => formal.argInfos match {
case arg1 :: arg2 :: Nil if !defn.isBottomClass(arg2.typeSymbol) =>
val tp1 = fullyDefinedType(arg1, "TypeTest argument", span)
val tp2 = fullyDefinedType(arg2, "TypeTest argument", span)
val sym2 = tp2.typeSymbol
if tp1 <:< tp2 then
ref(defn.TypeTestModule_identity).appliedToType(tp2).withSpan(span)
else if sym2 == defn.AnyValClass || sym2 == defn.AnyRefAlias || sym2 == defn.ObjectClass then
EmptyTree
else
// Generate SAM: (s: <tp1>) => if s.isInstanceOf[s.type & <tp2>] then Some(s.asInstanceOf[s.type & <tp2>]) else None
def body(args: List[Tree]): Tree = {
val arg :: Nil = args
val t = arg.tpe & tp2
If(
arg.select(defn.Any_isInstanceOf).appliedToType(t),
ref(defn.SomeClass.companionModule.termRef).select(nme.apply)
.appliedToType(t)
.appliedTo(arg.select(nme.asInstanceOf_).appliedToType(t)),
ref(defn.NoneModule))
}
val tpe = MethodType(List(nme.s))(_ => List(tp1), mth => defn.OptionClass.typeRef.appliedTo(mth.newParamRef(0) & tp2))
val meth = newSymbol(ctx.owner, nme.ANON_FUN, Synthetic | Method, tpe, coord = span)
val typeTestType = defn.TypeTestClass.typeRef.appliedTo(List(tp1, tp2))
Closure(meth, tss => body(tss.head).changeOwner(ctx.owner, meth), targetType = typeTestType).withSpan(span)
case _ =>
EmptyTree
}
end synthesizedTypeTest

val synthesizedTupleFunction: SpecialHandler = (formal, span) =>
formal match
case AppliedType(_, funArgs @ fun :: tupled :: Nil) =>
Expand Down Expand Up @@ -374,6 +405,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):

val specialHandlers = List(
defn.ClassTagClass -> synthesizedClassTag,
defn.TypeTestClass -> synthesizedTypeTest,
defn.EqlClass -> synthesizedEql,
defn.TupledFunctionClass -> synthesizedTupleFunction,
defn.ValueOfClass -> synthesizedValueOf,
Expand Down
23 changes: 13 additions & 10 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -787,15 +787,18 @@ class Typer extends Namer
*/
def tryWithClassTag(tree: Typed, pt: Type)(using Context): Tree = tree.tpt.tpe.dealias match {
case tref: TypeRef if !tref.symbol.isClass && !ctx.isAfterTyper && !(tref =:= pt) =>
require(ctx.mode.is(Mode.Pattern))
withoutMode(Mode.Pattern)(
inferImplicit(defn.ClassTagClass.typeRef.appliedTo(tref), EmptyTree, tree.tpt.span)
) match {
case SearchSuccess(clsTag, _, _) =>
typed(untpd.Apply(untpd.TypedSplice(clsTag), untpd.TypedSplice(tree.expr)), pt)
case _ =>
tree
def withTag(tpe: Type): Option[Tree] = {
require(ctx.mode.is(Mode.Pattern))
withoutMode(Mode.Pattern)(
inferImplicit(tpe, EmptyTree, tree.tpt.span)
) match
case SearchSuccess(clsTag, _, _) =>
Some(typed(untpd.Apply(untpd.TypedSplice(clsTag), untpd.TypedSplice(tree.expr)), pt))
case _ =>
None
}
withTag(defn.TypeTestClass.typeRef.appliedTo(pt, tref)).orElse(
withTag(defn.ClassTagClass.typeRef.appliedTo(tref))).getOrElse(tree)
case _ => tree
}

Expand Down Expand Up @@ -1838,8 +1841,8 @@ class Typer extends Namer
val body1 = typed(tree.body, pt)
body1 match {
case UnApply(fn, Nil, arg :: Nil)
if fn.symbol.exists && fn.symbol.owner == defn.ClassTagClass && !body1.tpe.isError =>
// A typed pattern `x @ (e: T)` with an implicit `ctag: ClassTag[T]`
if fn.symbol.exists && (fn.symbol.owner == defn.ClassTagClass || fn.symbol.owner.derivesFrom(defn.TypeTestClass)) && !body1.tpe.isError =>
// A typed pattern `x @ (e: T)` with an implicit `ctag: ClassTag[T]` or `ctag: TypeTest[T]`
// was rewritten to `x @ ctag(e)` by `tryWithClassTag`.
// Rewrite further to `ctag(x @ e)`
tpd.cpy.UnApply(body1)(fn, Nil,
Expand Down
70 changes: 70 additions & 0 deletions docs/docs/reference/other-new-features/type-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
layout: doc-page
title: "TypeTest"
---

TypeTest
--------

`TypeTest` provides a replacement for `ClassTag.unapply` where the type of the argument is generalized.
`TypeTest.unapply` will return `Some(x.asInstanceOf[Y])` if `x` conforms to `Y`, otherwise it returns `None`.

```scala
trait TypeTest[-S, T] extends Serializable {
def unapply(s: S): Option[s.type & T]
}
```

Just like `ClassTag` used to do, it can be used to perform type checks in patterns.

```scala
type X
type Y <: X
given TypeTest[X, Y] = ...
(x: X) match {
case y: Y => ... // safe checked downcast
case _ => ...
}
```


Examples
--------

Given the following abstract definition of `Peano` numbers that provides `TypeTest[Nat, Zero]` and `TypeTest[Nat, Succ]`

```scala
trait Peano {
type Nat
type Zero <: Nat
type Succ <: Nat
def safeDiv(m: Nat, n: Succ): (Nat, Nat)
val Zero: Zero
val Succ: SuccExtractor
trait SuccExtractor {
def apply(nat: Nat): Succ
def unapply(nat: Succ): Option[Nat]
}
given TypeTest[Nat, Zero] = typeTestOfZero
protected def typeTestOfZero: TypeTest[Nat, Zero]
given TypeTest[Nat, Succ]
protected def typeTestOfSucc: TypeTest[Nat, Succ]
```

it will be possible to write the following program

```scala
val peano: Peano = ...
import peano.{_, given}
def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = {
n match {
case Zero => None
case s @ Succ(_) => Some(safeDiv(m, s))
}
}
val two = Succ(Succ(Zero))
val five = Succ(Succ(Succ(two)))
println(divOpt(five, two))
```

Note that without the `TypeTest[Nat, Succ]` the pattern `Succ.unapply(nat: Succ)` would be unchecked.
31 changes: 31 additions & 0 deletions library/src/scala/reflect/TypeTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package scala.reflect

/** A `TypeTest[S, T] contains the logic needed to know at runtime if a value of
* type `S` can be downcasted to `T`.
*
* If a pattern match is performed on a term of type `s: S` that is uncheckable with `s.isInstanceOf[T]` and
* the pattern are of the form:
* - `t: T`
* - `t @ X()` where the `X.unapply` has takes an argument of type `T`
* then a given instance of `TypeTest[S, T]` is summoned and used to perform the test.
*/
@scala.annotation.implicitNotFound(msg = "No TypeTest available for [${S}, ${T}]")
trait TypeTest[-S, T] extends Serializable {

/** 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 `tt` 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]

}

object TypeTest {

def identity[T]: TypeTest[T, T] = Some(_)

}
24 changes: 24 additions & 0 deletions tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import scala.reflect.ClassTag

object IsInstanceOfClassTag {
def safeCast[T: ClassTag](x: Any): Option[T] = {
x match {
case x: T => Some(x) // TODO error: deprecation waring
case _ => None
}
}

def main(args: Array[String]): Unit = {
safeCast[List[String]](List[Int](1)) match {
case None =>
case Some(xs) =>
xs.head.substring(0)
}

safeCast[List[_]](List[Int](1)) match {
case None =>
case Some(xs) =>
xs.head.substring(0) // error
}
}
}
22 changes: 22 additions & 0 deletions tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.reflect.TypeTest

object IsInstanceOfClassTag {
def safeCast[T](x: Any)(using TypeTest[Any, T]): Option[T] = {
x match {
case x: T => Some(x)
case _ => None
}
}

def main(args: Array[String]): Unit = {
safeCast[List[String]](List[Int](1)) match { // error
case None =>
case Some(xs) =>
}

safeCast[List[_]](List[Int](1)) match {
case None =>
case Some(xs) =>
}
}
}
41 changes: 41 additions & 0 deletions tests/neg-custom-args/fatal-warnings/type-test-paths-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import scala.reflect.TypeTest

trait R {
type Nat
type Succ <: Nat
type Idx
given TypeTest[Nat, Succ] = typeTestOfSucc
protected def typeTestOfSucc: TypeTest[Nat, Succ]
def n: Nat
def one: Succ
}

object RI extends R {
type Nat = Int
type Succ = Int
type Idx = Int
protected def typeTestOfSucc: TypeTest[Nat, Succ] = new {
def unapply(x: Int): Option[x.type & Succ] =
if x > 0 then Some(x) else None
}
def n: Nat = 4
def one: Succ = 1
}

object Test {
val r1: R = RI
val r2: R = RI

r1.n match {
case n: r2.Nat => // error: the type test for Test.r2.Nat cannot be checked at runtime
case n: r1.Idx => // error: the type test for Test.r1.Idx cannot be checked at runtime
case n: r1.Succ => // Ok
case n: r1.Nat => // Ok
}

r1.one match {
case n: r2.Nat => // error: the type test for Test.r2.Nat cannot be checked at runtime
case n: r1.Idx => // error: the type test for Test.r1.Idx cannot be checked at runtime
case n: r1.Nat => // Ok
}
}
37 changes: 37 additions & 0 deletions tests/neg-custom-args/fatal-warnings/type-test-paths.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import scala.reflect.TypeTest

object Test {
def main(args: Array[String]): Unit = {
val p1: T = T1
val p2: T = T1

(p1.y: p1.X) match {
case x: p2.Y => // error: unchecked
case x: p1.Y =>
case _ =>
}
}

}

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 unapply(x: X): Option[x.type & Y] = x match
case x: (true & x.type) => Some(x)
case _ => None
}

}
29 changes: 29 additions & 0 deletions tests/neg-custom-args/fatal-warnings/type-test-syntesize.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import scala.reflect.TypeTest

object Test {
def test[S, T](using TypeTest[S, T]): Unit = ()
val a: A = ???

test[Any, Any]
test[Int, Int]

test[Int, Any]
test[String, Any]
test[String, AnyRef]

test[Any, Int]
test[Any, String]
test[Any, Some[_]]
test[Any, Array[Int]]
test[Seq[Int], List[Int]]

test[Any, Some[Int]] // error
test[Any, a.X] // error
test[a.X, a.Y] // error

}

class A {
type X
type Y <: X
}
19 changes: 19 additions & 0 deletions tests/neg/type-test-syntesize.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import scala.reflect.TypeTest

object Test {
def test[S, T](using x: TypeTest[S, T]): Unit = ()

test[Any, AnyRef] // error
test[Any, AnyVal] // error
test[Any, Object] // error

test[Any, Nothing] // error
test[AnyRef, Nothing] // error
test[AnyVal, Nothing] // error
test[Null, Nothing] // error
test[Unit, Nothing] // error
test[Int, Nothing] // error
test[8, Nothing] // error
test[List[_], Nothing] // error
test[Nothing, Nothing] // error
}
Loading

0 comments on commit 7f9d8dd

Please sign in to comment.