From 0bb5301f681c8919fa91abf1c86ec1f977704e22 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Wed, 13 Nov 2019 21:22:38 +0100 Subject: [PATCH 1/3] Fix #7554: Implement TypeTest interface `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. --- .../dotty/tools/dotc/core/Definitions.scala | 2 + .../dotty/tools/dotc/typer/Applications.scala | 2 +- .../dotty/tools/dotc/typer/Synthesizer.scala | 33 ++++ .../src/dotty/tools/dotc/typer/Typer.scala | 39 +++-- .../changed-features/pattern-matching.md | 11 +- .../reference/other-new-features/type-test.md | 146 ++++++++++++++++++ library/src/scala/reflect/TypeTest.scala | 28 ++++ library/src/scala/reflect/Typeable.scala | 12 ++ .../fatal-warnings/IsInstanceOfClassTag.scala | 24 +++ .../IsInstanceOfClassTag2.scala | 22 +++ .../classtag-typetest/3_0-migration.scala | 6 + .../classtag-typetest/3_0.scala | 6 + .../classtag-typetest/3_1-migration.scala | 6 + .../classtag-typetest/3_1.scala | 6 + .../fatal-warnings/type-test-paths-2.scala | 41 +++++ .../fatal-warnings/type-test-paths.scala | 37 +++++ .../fatal-warnings/type-test-syntesize.scala | 29 ++++ tests/neg/type-test-syntesize.scala | 19 +++ .../pos/classtag-typetest/3_0-migration.scala | 6 + tests/pos/classtag-typetest/3_0.scala | 6 + .../pos/classtag-typetest/3_1-migration.scala | 6 + tests/pos/classtag-typetest/3_1.scala | 6 + tests/pos/type-test-syntesize.scala | 29 ++++ tests/run/typable.scala | 11 ++ tests/run/type-test-binding.check | 2 + tests/run/type-test-binding.scala | 34 ++++ tests/run/type-test-nat.check | 6 + tests/run/type-test-nat.scala | 130 ++++++++++++++++ 28 files changed, 687 insertions(+), 18 deletions(-) create mode 100644 docs/docs/reference/other-new-features/type-test.md create mode 100644 library/src/scala/reflect/TypeTest.scala create mode 100644 library/src/scala/reflect/Typeable.scala create mode 100644 tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag.scala create mode 100644 tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag2.scala create mode 100644 tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0-migration.scala create mode 100644 tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0.scala create mode 100644 tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1-migration.scala create mode 100644 tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1.scala create mode 100644 tests/neg-custom-args/fatal-warnings/type-test-paths-2.scala create mode 100644 tests/neg-custom-args/fatal-warnings/type-test-paths.scala create mode 100644 tests/neg-custom-args/fatal-warnings/type-test-syntesize.scala create mode 100644 tests/neg/type-test-syntesize.scala create mode 100644 tests/pos/classtag-typetest/3_0-migration.scala create mode 100644 tests/pos/classtag-typetest/3_0.scala create mode 100644 tests/pos/classtag-typetest/3_1-migration.scala create mode 100644 tests/pos/classtag-typetest/3_1.scala create mode 100644 tests/pos/type-test-syntesize.scala create mode 100644 tests/run/typable.scala create mode 100644 tests/run/type-test-binding.check create mode 100644 tests/run/type-test-binding.scala create mode 100644 tests/run/type-test-nat.check create mode 100644 tests/run/type-test-nat.scala diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 329494f150aa..46e3b1bf2a50 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -791,6 +791,8 @@ class Definitions { @tu lazy val ClassTagModule: Symbol = ClassTagClass.companionModule @tu lazy val ClassTagModule_apply: Symbol = ClassTagModule.requiredMethod(nme.apply) + @tu lazy val TypeTestClass: ClassSymbol = requiredClass("scala.reflect.TypeTest") + @tu lazy val TypeTestModule_identity: Symbol = TypeTestClass.companionModule.requiredMethod(nme.identity) @tu lazy val QuotedExprClass: ClassSymbol = requiredClass("scala.quoted.Expr") @tu lazy val QuotedExprModule: Symbol = QuotedExprClass.companionModule diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index bb4dc7aece23..bda9f0c98a8f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -1331,7 +1331,7 @@ trait Applications extends Compatibility { val result = assignType(cpy.UnApply(tree)(unapplyFn, unapplyImplicits(unapplyApp), unapplyPatterns), ownType) unapp.println(s"unapply patterns = $unapplyPatterns") if ((ownType eq selType) || ownType.isError) result - else tryWithClassTag(Typed(result, TypeTree(ownType)), selType) + else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType) case tp => val unapplyErr = if (tp.isError) unapplyFn else notAnExtractor(unapplyFn) val typedArgsErr = args mapconserve (typed(_, defn.AnyType)) diff --git a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala index bb34a230bf49..95b921a06990 100644 --- a/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Synthesizer.scala @@ -45,6 +45,38 @@ 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 + // optimization when we know the typetest will always succeed + 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: ) => if s.isInstanceOf[] then Some(s.asInstanceOf[s.type & ]) else None + def body(args: List[Tree]): Tree = { + val arg :: Nil = args + val t = arg.tpe & tp2 + If( + arg.select(defn.Any_isInstanceOf).appliedToType(tp2), + 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) => @@ -374,6 +406,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, diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index a8ae701e3a27..b943316118a7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -773,7 +773,7 @@ class Typer extends Namer TypeComparer.constrainPatternType(tpt1.tpe, pt) } // special case for an abstract type that comes with a class tag - tryWithClassTag(ascription(tpt1, isWildcard = true), pt) + tryWithTypeTest(ascription(tpt1, isWildcard = true), pt) } cases( ifPat = handlePattern, @@ -782,21 +782,28 @@ class Typer extends Namer } } - /** For a typed tree `e: T`, if `T` is an abstract type for which an implicit class tag `ctag` - * exists, rewrite to `ctag(e)`. + /** For a typed tree `e: T`, if `T` is an abstract type for which an implicit type test or class tag `tt` + * exists, rewrite to `tt(e)`. * @pre We are in pattern-matching mode (Mode.Pattern) */ - def tryWithClassTag(tree: Typed, pt: Type)(using Context): Tree = tree.tpt.tpe.dealias match { + def tryWithTypeTest(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 } + val tag = withTag(defn.TypeTestClass.typeRef.appliedTo(pt, tref)) + .orElse(withTag(defn.ClassTagClass.typeRef.appliedTo(tref))) + .getOrElse(tree) + if tag.symbol.owner == defn.ClassTagClass && config.Feature.sourceVersion.isAtLeast(config.SourceVersion.`3.1`) then + report.warning("Use of ClassTag for type testing may be unsound. Consider using `reflect.Typable` instead.", tree.srcPos) + tag case _ => tree } @@ -1839,10 +1846,10 @@ 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]` - // was rewritten to `x @ ctag(e)` by `tryWithClassTag`. - // Rewrite further to `ctag(x @ e)` + if fn.symbol.exists && (fn.symbol.owner.derivesFrom(defn.TypeTestClass) || fn.symbol.owner == defn.ClassTagClass) && !body1.tpe.isError => + // A typed pattern `x @ (e: T)` with an implicit `tt: TypeTest[T]` or `ctag: ClassTag[T]` + // was rewritten to `x @ tt(e)` `x @ ctag(e)` by `tryWithTypeTest`. + // Rewrite further to `tt(x @ e)` or `ctag(x @ e)` tpd.cpy.UnApply(body1)(fn, Nil, typed(untpd.Bind(tree.name, untpd.TypedSplice(arg)).withSpan(tree.span), arg.tpe) :: Nil) case _ => diff --git a/docs/docs/reference/changed-features/pattern-matching.md b/docs/docs/reference/changed-features/pattern-matching.md index 6f88fd06348d..14628b0e36f1 100644 --- a/docs/docs/reference/changed-features/pattern-matching.md +++ b/docs/docs/reference/changed-features/pattern-matching.md @@ -240,4 +240,13 @@ def foo(f: Foo) = f match { ``` There are plans for further simplification, in particular to factor out *product -match* and *name-based match* into a single type of extractor. \ No newline at end of file +match* and *name-based match* into a single type of extractor. + +## Type testing + +Abstract type testing with `ClassTag` is replaced with `TypeTest` or the alias `Typeable`. + +- pattern `_: X` for an abstract type requires a `TypeTest` in scope +- pattern `x @ X()` for an unapply that takes an abstract type requires a `TypeTest` in scope + +[More details on TypeTest](../other-new-features/type-test.md) diff --git a/docs/docs/reference/other-new-features/type-test.md b/docs/docs/reference/other-new-features/type-test.md new file mode 100644 index 000000000000..101f9393ca6b --- /dev/null +++ b/docs/docs/reference/other-new-features/type-test.md @@ -0,0 +1,146 @@ +--- +layout: doc-page +title: "TypeTest" +--- + +TypeTest +-------- + +When pattern matching there are two situations where were a runtime type test must be performed. +The first is kind is an explicit type test using the ascription pattern notation. +```scala +(x: X) match + case y: Y => +``` +The second is when an extractor takes an argument that is not a subtype of the scrutinee type. +```scala +(x: X) match + case y @ Y(n) => + +object Y: + def unapply(x: Y): Some[Int] = ... +``` + +In both cases, a class test will be performed at runtime. +But when the type test is on an abstract type (type parameter or type member), the test cannot be performed because the type is erased at runtime. + +A `TypeTest` can be provided to make this test possible. + +```scala +package scala.reflect + +trait TypeTest[-S, T]: + def unapply(s: S): Option[s.type & T] +``` + +It provides an extractor that returns its argument typed as a `T` if the argument is a `T`. +It can be used to encode a type test. +```scala +def f[X, Y](x: X)(using tt: TypeTest[X, Y]): Option[Y] = + x match + case tt(x @ Y(1)) => Some(x) + case tt(x) => Some(x) + case _ => None +``` + +To avoid the syntactic overhead the compiler will look for a type test automatically if it detects that the type test is on abstract types. +This means that `x: Y` is transformed to `tt(x)` and `x @ Y(_)` to `tt(x @ Y(_))` if there is a contextual `TypeTest[X, Y]` in scope. +The previous code is equivalent to + +```scala +def f[X, Y](x: X)(using TypeTest[X, Y]): Option[Y] = + x match + case x @ Y(1) => Some(x) + case x: Y => Some(x) + case _ => None +``` + +We could create a type test at call site where the type test can be performed with runtime class tests directly as follows + +```scala +val tt: TypeTest[Any, String] = + new TypeTest[Any, String] + def unapply(s: Any): Option[s.type & String] = + s match + case s: String => Some(s) + case _ => None + +f[AnyRef, String]("acb")(using tt) +``` + +The compiler will synthesize a new instance of a type test if non is found in scope as +```scala +new TypeTest[A, B]: + def unapply(s: A): Option[s.type & B] = + s match + case s: B => Some(s) + case _ => None +``` +If the type tests cannot be done there will be an unchecked warning that will be raised on the `case s: B =>` test. + +The most common `TypeTest` are the ones that take any parameters (i.e. `TypeTest[Any, T]`). +To make it possible to use this directly in context bounds we provide the alias +```scala +package scala.reflect + +type Typeable[T] = TypeTest[Any, T] +``` + +This alias can be used as + +```scala +def f[T: Typeable]: Boolean = + "abc" match + case x: T => true + case _ => false + +f[String] // true +f[Int] // fasle +``` + +### TypeTest and ClassTag +`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. + + +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. diff --git a/library/src/scala/reflect/TypeTest.scala b/library/src/scala/reflect/TypeTest.scala new file mode 100644 index 000000000000..7d3cc9908cc4 --- /dev/null +++ b/library/src/scala/reflect/TypeTest.scala @@ -0,0 +1,28 @@ +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: + + /** Trivial type test that always succeeds */ + def identity[T]: TypeTest[T, T] = Some(_) diff --git a/library/src/scala/reflect/Typeable.scala b/library/src/scala/reflect/Typeable.scala new file mode 100644 index 000000000000..3214de77123e --- /dev/null +++ b/library/src/scala/reflect/Typeable.scala @@ -0,0 +1,12 @@ +package scala.reflect + +/** A shorhand for `TypeTest[Any, T]`. A `Typeable[T] contains the logic needed to + * know at runtime if a value can be downcasted to `T`. + * + * If a pattern match is performed on a term of type `s: Any` 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 `Typeable[T]` (`TypeTest[Any, T]`) is summoned and used to perform the test. + */ +type Typeable[T] = TypeTest[Any, T] diff --git a/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag.scala b/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag.scala new file mode 100644 index 000000000000..a398d7bdaf24 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag.scala @@ -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 + } + } +} diff --git a/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag2.scala b/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag2.scala new file mode 100644 index 000000000000..d9782bc14f34 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/IsInstanceOfClassTag2.scala @@ -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) => + } + } +} diff --git a/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0-migration.scala b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0-migration.scala new file mode 100644 index 000000000000..8143941458e0 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0-migration.scala @@ -0,0 +1,6 @@ +import scala.language.`3.0-migration` +import scala.reflect.ClassTag + +def f3_0m[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0.scala b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0.scala new file mode 100644 index 000000000000..a8478f06c706 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_0.scala @@ -0,0 +1,6 @@ +import scala.language.`3.0` +import scala.reflect.ClassTag + +def f3_0[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1-migration.scala b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1-migration.scala new file mode 100644 index 000000000000..07bb2971bb0a --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1-migration.scala @@ -0,0 +1,6 @@ +import scala.language.`3.1-migration` +import scala.reflect.ClassTag + +def f3_1m[T: ClassTag](x: Any): Unit = + x match + case _: T => // error diff --git a/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1.scala b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1.scala new file mode 100644 index 000000000000..3a795e7e3dd0 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/classtag-typetest/3_1.scala @@ -0,0 +1,6 @@ +import scala.language.`3.1` +import scala.reflect.ClassTag + +def f3_1[T: ClassTag](x: Any): Unit = + x match + case _: T => // error diff --git a/tests/neg-custom-args/fatal-warnings/type-test-paths-2.scala b/tests/neg-custom-args/fatal-warnings/type-test-paths-2.scala new file mode 100644 index 000000000000..488f704a20bc --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/type-test-paths-2.scala @@ -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 + } +} diff --git a/tests/neg-custom-args/fatal-warnings/type-test-paths.scala b/tests/neg-custom-args/fatal-warnings/type-test-paths.scala new file mode 100644 index 000000000000..a8cd6334e769 --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/type-test-paths.scala @@ -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 + } + +} diff --git a/tests/neg-custom-args/fatal-warnings/type-test-syntesize.scala b/tests/neg-custom-args/fatal-warnings/type-test-syntesize.scala new file mode 100644 index 000000000000..45ef924ce55a --- /dev/null +++ b/tests/neg-custom-args/fatal-warnings/type-test-syntesize.scala @@ -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 +} diff --git a/tests/neg/type-test-syntesize.scala b/tests/neg/type-test-syntesize.scala new file mode 100644 index 000000000000..86801cd8bcef --- /dev/null +++ b/tests/neg/type-test-syntesize.scala @@ -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 +} diff --git a/tests/pos/classtag-typetest/3_0-migration.scala b/tests/pos/classtag-typetest/3_0-migration.scala new file mode 100644 index 000000000000..755bab5e5755 --- /dev/null +++ b/tests/pos/classtag-typetest/3_0-migration.scala @@ -0,0 +1,6 @@ +import scala.language.`3.0-migration` +import scala.reflect.ClassTag + +def f3_0m[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/pos/classtag-typetest/3_0.scala b/tests/pos/classtag-typetest/3_0.scala new file mode 100644 index 000000000000..a8478f06c706 --- /dev/null +++ b/tests/pos/classtag-typetest/3_0.scala @@ -0,0 +1,6 @@ +import scala.language.`3.0` +import scala.reflect.ClassTag + +def f3_0[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/pos/classtag-typetest/3_1-migration.scala b/tests/pos/classtag-typetest/3_1-migration.scala new file mode 100644 index 000000000000..0cd62d9cdb36 --- /dev/null +++ b/tests/pos/classtag-typetest/3_1-migration.scala @@ -0,0 +1,6 @@ +import scala.language.`3.1-migration` +import scala.reflect.ClassTag + +def f3_1m[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/pos/classtag-typetest/3_1.scala b/tests/pos/classtag-typetest/3_1.scala new file mode 100644 index 000000000000..12a3c2a3bb94 --- /dev/null +++ b/tests/pos/classtag-typetest/3_1.scala @@ -0,0 +1,6 @@ +import scala.language.`3.1` +import scala.reflect.ClassTag + +def f3_1[T: ClassTag](x: Any): Unit = + x match + case _: T => diff --git a/tests/pos/type-test-syntesize.scala b/tests/pos/type-test-syntesize.scala new file mode 100644 index 000000000000..de5540c6b0c1 --- /dev/null +++ b/tests/pos/type-test-syntesize.scala @@ -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]] // unchecked warning + test[Any, a.X] // unchecked warning + test[a.X, a.Y] // unchecked warning + +} + +class A { + type X + type Y <: X +} diff --git a/tests/run/typable.scala b/tests/run/typable.scala new file mode 100644 index 000000000000..7fe253023740 --- /dev/null +++ b/tests/run/typable.scala @@ -0,0 +1,11 @@ +import scala.reflect._ + +object Test: + def main(args: Array[String]): Unit = + assert(f[String]) + assert(!f[Int]) + + def f[T: Typeable]: Boolean = + "abc" match + case x: T => true + case _ => false diff --git a/tests/run/type-test-binding.check b/tests/run/type-test-binding.check new file mode 100644 index 000000000000..14e859cfe54b --- /dev/null +++ b/tests/run/type-test-binding.check @@ -0,0 +1,2 @@ +ok +9 diff --git a/tests/run/type-test-binding.scala b/tests/run/type-test-binding.scala new file mode 100644 index 000000000000..ad055a537f46 --- /dev/null +++ b/tests/run/type-test-binding.scala @@ -0,0 +1,34 @@ +import scala.reflect.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 unapply(x: X): Option[x.type & Y] = + Some(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 = { + foo.x match { + case x @ foo.Z(i) => // `x` is refined to type `foo.Y` + foo.f(x) + println(i) + } + } +} diff --git a/tests/run/type-test-nat.check b/tests/run/type-test-nat.check new file mode 100644 index 000000000000..5dc5e167eeae --- /dev/null +++ b/tests/run/type-test-nat.check @@ -0,0 +1,6 @@ +Some((SuccClass(SuccClass(ZeroObject)),SuccClass(ZeroObject))) +Some((ZeroObject,SuccClass(SuccClass(ZeroObject)))) +None +Some((2,1)) +Some((0,2)) +None diff --git a/tests/run/type-test-nat.scala b/tests/run/type-test-nat.scala new file mode 100644 index 000000000000..a7f21ec33fce --- /dev/null +++ b/tests/run/type-test-nat.scala @@ -0,0 +1,130 @@ +import scala.reflect.TypeTest + +object Test { + def main(args: Array[String]): Unit = { + app(ClassNums) + app(IntNums) + } + + def app(peano: Peano): Unit = { + import peano._ + 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)) + println(divOpt(two, five)) + println(divOpt(two, Zero)) + } +} + +trait Peano { + type Nat + + type Zero <: Nat + given TypeTest[Nat, Zero] = typeTestOfZero + + type Succ <: Nat + given TypeTest[Nat, Succ] = typeTestOfSucc + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) + + protected def typeTestOfZero: TypeTest[Nat, Zero] + protected def typeTestOfSucc: TypeTest[Nat, Succ] + + implicit def succDeco(succ: Succ): SuccAPI + trait SuccAPI { + def pred: Nat + } + + val Zero: Zero + + val Succ: SuccExtractor + trait SuccExtractor { + def apply(nat: Nat): Succ + def unapply(nat: Succ): Option[Nat] + } +} + +object IntNums extends Peano { + type Nat = Int + type Zero = Int + type Succ = Int + + protected def typeTestOfZero: TypeTest[Nat, Zero] = new { + def unapply(x: Nat): Option[x.type & Zero] = + if x == 0 then Some(x) + else None + } + + protected def typeTestOfSucc: TypeTest[Nat, Succ] = new { + def unapply(x: Nat): Option[x.type & Succ] = + if x > 0 then Some(x) + else None + } + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = (m / n, m % n) + + val Zero: Zero = 0 + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = nat + 1 + def unapply(nat: Succ) = Some(nat - 1) + } + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ - 1 + } +} + +object ClassNums extends Peano { + trait NatTrait + object ZeroObject extends NatTrait { + override def toString: String = "ZeroObject" + } + case class SuccClass(predecessor: NatTrait) extends NatTrait + + type Nat = NatTrait + type Zero = ZeroObject.type + type Succ = SuccClass + + protected def typeTestOfZero: TypeTest[Nat, Zero] = new { + def unapply(x: Nat): Option[x.type & Zero] = x match + case x: (ZeroObject.type & x.type) => Some(x) + case _ => None + } + + protected def typeTestOfSucc: TypeTest[Nat, Succ] = new { + def unapply(x: Nat): Option[x.type & Succ] = x match + case x: (SuccClass & x.type) => Some(x) + case _ => None + } + + def safeDiv(m: Nat, n: Succ): (Nat, Nat) = { + def intValue(x: Nat, acc: Int): Int = x match { + case nat: SuccClass => intValue(nat.predecessor, acc + 1) + case _ => acc + } + def natValue(x: Int): Nat = + if (x == 0) ZeroObject + else new SuccClass(natValue(x - 1)) + val i = intValue(m, 0) + val j = intValue(n, 0) + (natValue(i / j), natValue(i % j)) + } + + val Zero: Zero = ZeroObject + + object Succ extends SuccExtractor { + def apply(nat: Nat): Succ = new SuccClass(nat) + def unapply(nat: Succ) = Some(nat.predecessor) + } + + def succDeco(succ: Succ): SuccAPI = new SuccAPI { + def pred: Nat = succ.predecessor + } + +} From 5dec63f9ca6c1608e25440e04d5aea9f9111a249 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Fri, 6 Nov 2020 10:32:26 +0100 Subject: [PATCH 2/3] Improve docs --- .../reference/other-new-features/type-test.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/reference/other-new-features/type-test.md b/docs/docs/reference/other-new-features/type-test.md index 101f9393ca6b..bbb2016f1dee 100644 --- a/docs/docs/reference/other-new-features/type-test.md +++ b/docs/docs/reference/other-new-features/type-test.md @@ -68,7 +68,7 @@ val tt: TypeTest[Any, String] = f[AnyRef, String]("acb")(using tt) ``` -The compiler will synthesize a new instance of a type test if non is found in scope as +The compiler will synthesize a new instance of a type test if none is found in scope as: ```scala new TypeTest[A, B]: def unapply(s: A): Option[s.type & B] = @@ -78,8 +78,8 @@ new TypeTest[A, B]: ``` If the type tests cannot be done there will be an unchecked warning that will be raised on the `case s: B =>` test. -The most common `TypeTest` are the ones that take any parameters (i.e. `TypeTest[Any, T]`). -To make it possible to use this directly in context bounds we provide the alias +The most common `TypeTest` instances are the ones that take any parameters (i.e. `TypeTest[Any, T]`). +To make it possible to use such instances directly in context bounds we provide the alias ```scala package scala.reflect @@ -99,10 +99,10 @@ f[Int] // fasle ``` ### TypeTest and ClassTag -`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 replacement for functionality provided previously by `ClassTag.unapply`. +Using `ClassTag` instances was unsound since classtags can check only the class component of a type. +`TypeTest` fixes that unsoundness. +`ClassTag` type tests are still supported but a warning will be emitted after 3.0. Examples @@ -132,7 +132,7 @@ it will be possible to write the following program ```scala val peano: Peano = ... -import peano.{_, given _} +import peano._ def divOpt(m: Nat, n: Nat): Option[(Nat, Nat)] = n match case Zero => None From 919acc2ed5de3e14b40811216f7bcf22f248d665 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Fri, 6 Nov 2020 11:59:37 +0100 Subject: [PATCH 3/3] Update docs/docs/reference/other-new-features/type-test.md Co-authored-by: Jamie Thompson --- docs/docs/reference/other-new-features/type-test.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/reference/other-new-features/type-test.md b/docs/docs/reference/other-new-features/type-test.md index bbb2016f1dee..1bd066b02ffc 100644 --- a/docs/docs/reference/other-new-features/type-test.md +++ b/docs/docs/reference/other-new-features/type-test.md @@ -124,7 +124,7 @@ trait Peano: } given TypeTest[Nat, Zero] = typeTestOfZero protected def typeTestOfZero: TypeTest[Nat, Zero] - given TypeTest[Nat, Succ] + given TypeTest[Nat, Succ] = typeTestOfSucc protected def typeTestOfSucc: TypeTest[Nat, Succ] ```