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 Nov 15, 2019
1 parent ca6b1c6 commit a13d99d
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 9 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 @@ -631,6 +631,10 @@ class Definitions {
@tu lazy val ClassTagModule: Symbol = ClassTagClass.companionModule
@tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply)

@tu lazy val TypeTestClass: ClassSymbol = ctx.requiredClass("scala.tasty.TypeTest")
@tu lazy val TypeTestModule: Symbol = TypeTestClass.companionModule
@tu lazy val TypeTestModule_unapply: Symbol = TypeTestModule.requiredMethod(nme.unapply)

@tu lazy val QuotedExprClass: ClassSymbol = ctx.requiredClass("scala.quoted.Expr")
@tu lazy val QuotedExprModule: Symbol = QuotedExprClass.companionModule
@tu lazy val QuotedExprModule_nullExpr: Symbol = QuotedExprModule.requiredMethod(nme.nullExpr)
Expand Down
19 changes: 10 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -681,14 +681,15 @@ class Typer extends Namer
*/
def tryWithClassTag(tree: Typed, pt: Type)(implicit ctx: Context): Tree = tree.tpt.tpe.dealias match {
case tref: TypeRef if !tref.symbol.isClass && !ctx.isAfterTyper && !(tref =:= pt) =>
require(ctx.mode.is(Mode.Pattern))
inferImplicit(defn.ClassTagClass.typeRef.appliedTo(tref),
EmptyTree, tree.tpt.span)(ctx.retractMode(Mode.Pattern)) match {
case SearchSuccess(clsTag, _, _) =>
typed(untpd.Apply(untpd.TypedSplice(clsTag), untpd.TypedSplice(tree.expr)), pt)
case _ =>
tree
def withTag(tpe: Type): Option[Tree] = {
inferImplicit(tpe, EmptyTree, tree.tpt.span)(ctx.retractMode(Mode.Pattern)) match
case SearchSuccess(typeTest, _, _) =>
Some(typed(untpd.Apply(untpd.TypedSplice(typeTest), 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 @@ -1491,8 +1492,8 @@ class Typer extends Namer
val body1 = typed(tree.body, pt1)
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 the 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 <: S] extends Serializable {
def unapply(s: S): Option[s.type & T]
}
```

Just like `ClassTag` used to to, 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.
41 changes: 41 additions & 0 deletions library/src/scala/tasty/TypeTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package scala.tasty

/** A `TypeTest[S, T]` (where `T <: S`) contains the logic needed to know at runtime if a value of
* type `S` can be downcased 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 performed the test.
*
* Note: This is replacemet for `ClassTag.unapply` that can be sound for path dependent types
*/
@scala.annotation.implicitNotFound(msg = "No TypeTest available for [${S}, ${T}]")
trait TypeTest[S, T <: S] extends Serializable {

def isInstance(x: S): TypeTest.Result[x.type & 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] =
if isInstance(x).asInstanceOf[Boolean] then Some(x.asInstanceOf[x.type & T])
else None

}

object TypeTest {

opaque type Result[A] = Boolean

def success[A](x: A): Result[A] = true

def failure[A]: Result[A] = false

}
44 changes: 44 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,44 @@
import scala.tasty.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 isInstance(x: Int): TypeTest.Result[x.type & Succ] =
if x > 0 then TypeTest.success(x) else TypeTest.failure
}
def n: Nat = 4
def one: Succ = 1
}

object Test {
val r1: R = RI
import r1.given

val r2: R = RI
import r2.given

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
}
}
40 changes: 40 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,40 @@
import scala.tasty.TypeTest

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 => // 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 isInstance(x: X): TypeTest.Result[x.type & Y] = x match
case x: (true & x.type) => TypeTest.success(x)
case _ => TypeTest.failure
}

}
2 changes: 2 additions & 0 deletions tests/run/type-test-binding.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ok
9
35 changes: 35 additions & 0 deletions tests/run/type-test-binding.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import scala.tasty.TypeTest

sealed trait Foo {

type X
type Y <: X

def x: X

def f(y: Y) = println("ok")

given TypeTest[X, Y] = new TypeTest {
def isInstance(x: X): TypeTest.Result[x.type & Y] =
TypeTest.success(x.asInstanceOf[x.type & Y])
}

object Z {
def unapply(arg: Y): Option[Int] = Some(9)
}
}

object Test {
def main(args: Array[String]): Unit = {
test(new Foo { type X = Int; type Y = Int; def x: X = 1 })
}

def test(foo: Foo): Unit = {
import foo.given
foo.x match {
case x @ foo.Z(i) => // `x` is refined to type `foo.Y`
foo.f(x)
println(i)
}
}
}
6 changes: 6 additions & 0 deletions tests/run/type-test-nat.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Some((SuccClass(SuccClass(ZeroObject)),SuccClass(ZeroObject)))
Some((ZeroObject,SuccClass(SuccClass(ZeroObject))))
None
Some((2,1))
Some((0,2))
None
Loading

0 comments on commit a13d99d

Please sign in to comment.