Skip to content

Commit

Permalink
turn unchecked type patterns into checked ones
Browse files Browse the repository at this point in the history
the pattern `(_: T)` is made checkable using (ct: ClassTag[T]).unapply
by rewriting it to `ct(_: T)` (if there's a ClassTag[T] available)

similarly for extractors: if the formal type of the unapply method
is an uncheckable type, wrap in the corresponding classtag extractor (if available)

don't trigger rewrite on non-toplevel unchecked types
(i.e., only look at type constructor part of T when looking for unchecked types)

TODO: find outer match to figure out if we're supposed to be unchecked

would like to give users a chance to opt-out from the wrapping,
but finding the match to which this pattern belongs turned out to be tricky...
  • Loading branch information
adriaanm authored and xeno-by committed Jun 8, 2012
1 parent 10292e4 commit 6b3ef4f
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 20 deletions.
69 changes: 63 additions & 6 deletions src/compiler/scala/tools/nsc/typechecker/Typers.scala
Expand Up @@ -970,7 +970,7 @@ trait Typers extends Modes with Adaptations with Tags {
val overloadedExtractorOfObject = tree.symbol filter (sym => hasUnapplyMember(sym.tpe))
// if the tree's symbol's type does not define an extractor, maybe the tree's type does
// this is the case when we encounter an arbitrary tree as the target of an unapply call (rather than something that looks like a constructor call)
// (for now, this only happens due to maybeTypeTagExtractor, but when we support parameterized extractors, it will become more common place)
// (for now, this only happens due to maybeWrapClassTagUnapply, but when we support parameterized extractors, it will become more common place)
val extractor = overloadedExtractorOfObject orElse unapplyMember(tree.tpe)
if (extractor != NoSymbol) {
// if we did some ad-hoc overloading resolution, update the tree's symbol
Expand Down Expand Up @@ -3120,12 +3120,64 @@ trait Typers extends Modes with Adaptations with Tags {

val itype = glb(List(pt1, arg.tpe))
arg.tpe = pt1 // restore type (arg is a dummy tree, just needs to pass typechecking)
UnApply(fun1, args1) setPos tree.pos setType itype
val unapply = UnApply(fun1, args1) setPos tree.pos setType itype

// if the type that the unapply method expects for its argument is uncheckable, wrap in classtag extractor
// skip if the unapply's type is not a method type with (at least, but really it should be exactly) one argument
if (unappType.paramTypes.isEmpty) unapply
else maybeWrapClassTagUnapply(unapply, unappType.paramTypes.head)
} else
duplErrorTree(WrongNumberArgsPatternError(tree, fun))
}
}

// TODO: disable when in unchecked match
def maybeWrapClassTagUnapply(uncheckedPattern: Tree, pt: Type): Tree = if (isPastTyper) uncheckedPattern else {
// only look at top-level type, can't (reliably) do anything about unchecked type args (in general)
pt.normalize.typeConstructor match {
// if at least one of the types in an intersection is checkable, use the checkable ones
// this avoids problems as in run/matchonseq.scala, where the expected type is `Coll with scala.collection.SeqLike`
// Coll is an abstract type, but SeqLike of course is not
case tp@RefinedType(parents, _) if (parents.length >= 2) && (parents.exists(tp => !infer.containsUnchecked(tp))) =>
uncheckedPattern

case ptCheckable if infer.containsUnchecked(ptCheckable) =>
uncheckedPattern match {
// are we being called on a classtag extractor call?
// don't recurse forever, it's not *that* much fun
case UnApply(fun, _) if fun.symbol.owner.isNonBottomSubClass(ClassTagClass) =>
uncheckedPattern

case _ =>
val typeTagExtractor = resolveClassTag(uncheckedPattern.pos, ptCheckable)
//orElse resolveTypeTag(uncheckedPattern.pos, ReflectMirrorPrefix.tpe, ptCheckable, concrete = true)

// we don't create a new Context for a Match, so find the CaseDef, then go out one level and navigate back to the match that has this case
// val thisCase = context.nextEnclosing(_.tree.isInstanceOf[CaseDef])
// val unchecked = thisCase.outer.tree.collect{case Match(selector, cases) if cases contains thisCase => selector} match {
// case List(Typed(_, tpt)) if treeInfo.isUncheckedAnnotation(tpt.tpe) => true
// case t => println("outer tree: "+ (t, thisCase, thisCase.outer.tree)); false
// }
// println("maybeWrapClassTagUnapply"+ (!isPastTyper && infer.containsUnchecked(pt), pt, uncheckedPattern))
// println("typeTagExtractor"+ typeTagExtractor)


if (typeTagExtractor != EmptyTree && unapplyMember(typeTagExtractor.tpe) != NoSymbol) {
// println("maybeWrapClassTagUnapply "+ typeTagExtractor)
// println(util.Position.formatMessage(uncheckedPattern.pos, "made unchecked type test into a checked one", true))
val args = List(uncheckedPattern)
// must call doTypedUnapply directly, as otherwise we get undesirable rewrites
// and re-typechecks of the target of the unapply call in PATTERNmode,
// this breaks down when the extractor (which defineds the unapply member) is not a simple reference to an object,
// but an arbitrary tree as is the case here
doTypedUnapply(Apply(typeTagExtractor, args), typeTagExtractor, typeTagExtractor, args, PATTERNmode, pt)
} else uncheckedPattern
}

case _ => uncheckedPattern
}
}

/**
* Convert an annotation constructor call into an AnnotationInfo.
*
Expand Down Expand Up @@ -4823,10 +4875,15 @@ trait Typers extends Modes with Adaptations with Tags {
}

case Typed(expr, tpt) =>
val tpt1 = typedType(tpt, mode)
val expr1 = typed(expr, onlyStickyModes(mode), tpt1.tpe.deconst)
val ownType = if (isPatternMode) inferTypedPattern(tpt1, tpt1.tpe, pt) else tpt1.tpe
treeCopy.Typed(tree, expr1, tpt1) setType ownType
val tptTyped = typedType(tpt, mode)
val exprTyped = typed(expr, onlyStickyModes(mode), tptTyped.tpe.deconst)
val treeTyped = treeCopy.Typed(tree, exprTyped, tptTyped)

if (isPatternMode) {
val ownType = inferTypedPattern(tptTyped, tptTyped.tpe, pt)
maybeWrapClassTagUnapply(treeTyped setType ownType, tptTyped.tpe)
} else
treeTyped setType tptTyped.tpe

case TypeApply(fun, args) =>
// @M: kind-arity checking is done here and in adapt, full kind-checking is in checkKindBounds (in Infer)
Expand Down
10 changes: 10 additions & 0 deletions src/library/scala/reflect/ClassTag.scala
Expand Up @@ -44,6 +44,16 @@ trait ClassTag[T] extends Equals with Serializable {
case _ => java.lang.reflect.Array.newInstance(runtimeClass, len).asInstanceOf[Array[T]]
}

/** A ClassTag[T] can serve as an extractor that matches only objects of type T.
*
* The compiler tries to turn unchecked type tests in pattern matches into checked ones
* by wrapping a `(_: T)` type pattern as `ct(_: T)`, where `ct` is the `ClassTag[T]` instance.
* Type tests necessary before calling other extractors are treated similarly.
* `SomeExtractor(...)` is turned into `ct(SomeExtractor(...))` if `T` in `SomeExtractor.unapply(x: T)`
* is uncheckable, but we have an instance of `ClassTag[T]`.
*/
def unapply(x: Any): Option[T] = if (runtimeClass.isAssignableFrom(x.getClass)) Some(x.asInstanceOf[T]) else None

/** case class accessories */
override def canEqual(x: Any) = x.isInstanceOf[ClassTag[_]]
override def equals(x: Any) = x.isInstanceOf[ClassTag[_]] && this.runtimeClass == x.asInstanceOf[ClassTag[_]].runtimeClass
Expand Down
21 changes: 7 additions & 14 deletions test/files/run/patmat_unapp_abstype-new.scala
@@ -1,10 +1,10 @@
import reflect.{ClassTag, classTag}

// abstract types and extractors, oh my!
trait TypesAPI {
trait Type

// an alternative fix (implemented in the virtual pattern matcher, is to replace the isInstanceOf by a manifest-based run-time test)
// that's what typeRefMani is for
type TypeRef <: Type //; implicit def typeRefMani: Manifest[TypeRef]
type TypeRef <: Type
val TypeRef: TypeRefExtractor; trait TypeRefExtractor {
def apply(x: Int): TypeRef
def unapply(x: TypeRef): Option[(Int)]
Expand All @@ -19,11 +19,6 @@ trait TypesUser extends TypesAPI {
def shouldNotCrash(tp: Type): Unit = {
tp match {
case TypeRef(x) => println("TypeRef")
// the above checks tp.isInstanceOf[TypeRef], which is erased to tp.isInstanceOf[Type]
// before calling TypeRef.unapply(tp), which will then crash unless tp.isInstanceOf[TypesImpl#TypeRef] (which is not implied by tp.isInstanceOf[Type])
// tp.isInstanceOf[TypesImpl#TypeRef] is equivalent to classOf[TypesImpl#TypeRef].isAssignableFrom(tp.getClass)
// this is equivalent to manifest
// it is NOT equivalent to manifest[Type] <:< typeRefMani
case MethodType(x) => println("MethodType")
case _ => println("none of the above")
}
Expand All @@ -34,7 +29,6 @@ trait TypesImpl extends TypesAPI {
object TypeRef extends TypeRefExtractor // this will have a bridged unapply(x: Type) = unapply(x.asInstanceOf[TypeRef])
case class TypeRef(n: Int) extends Type // this has a bridge from TypesAPI#Type to TypesImpl#TypeRef
// --> the cast in the bridge will fail because the pattern matcher can't type test against the abstract types in TypesUser
//lazy val typeRefMani = manifest[TypeRef]
}

trait Foos {
Expand Down Expand Up @@ -63,16 +57,15 @@ trait Intermed extends Foos {

object TestUnappStaticallyKnownSynthetic extends TypesImpl with TypesUser {
def test() = {
shouldNotCrash(TypeRef(10)) // should and does print "TypeRef"
// once #1697/#2337 are fixed, this should generate the correct output
shouldNotCrash(MethodType(10)) // should print "MethodType" but prints "none of the above" -- good one, pattern matcher!
shouldNotCrash(TypeRef(10)) // prints "TypeRef"
shouldNotCrash(MethodType(10)) // prints "MethodType"
}
}

object TestUnappDynamicSynth extends RealFoos with Intermed {
case class FooToo(n: Int) extends Bar
case class NotAFoo(n: Int) extends Bar
def test() = {
crash(FooToo(10))
crash(NotAFoo(10))
crash(new Foo(5))
}
}
Expand Down
10 changes: 10 additions & 0 deletions test/files/run/virtpatmat_typetag.check
@@ -0,0 +1,10 @@
1 is not a ClassTag[int]; it's a class java.lang.Integer
1 is a ClassTag[class java.lang.Integer]
1 is not a ClassTag[class java.lang.String]; it's a class java.lang.Integer
true is a ClassTag[class java.lang.Object]
woele is a ClassTag[class java.lang.String]
1 is not a ClassTag[int]; it's a class java.lang.Integer
1 is a ClassTag[class java.lang.Integer]
1 is not a ClassTag[class java.lang.String]; it's a class java.lang.Integer
true is a ClassTag[class java.lang.Object]
woele is a ClassTag[class java.lang.String]
36 changes: 36 additions & 0 deletions test/files/run/virtpatmat_typetag.scala
@@ -0,0 +1,36 @@
import reflect.{ClassTag, classTag}

trait Extractors {
type T
implicit val tTag: ClassTag[T]
object ExtractT {
def unapply(x: T) = Some(x)
}
def apply(a: Any) = a match {
case ExtractT(x) => println(x +" is a "+ implicitly[ClassTag[T]])
case _ => println(a+ " is not a "+ implicitly[ClassTag[T]] +"; it's a "+ a.getClass)
}
}

object Test extends App {
def typeMatch[T: ClassTag](a: Any) = a match {
case x : T => println(x +" is a "+ implicitly[ClassTag[T]])
case _ => println(a+ " is not a "+ implicitly[ClassTag[T]] +"; it's a "+ a.getClass)
}

// the same match as typeMatch, but using an extractor
def extractorMatch[S: ClassTag](a: Any) =
(new Extractors { type T = S; val tTag = classTag[T] })(a)

typeMatch[Int](1)
typeMatch[Integer](1)
typeMatch[String](1)
typeMatch[Any](true)
typeMatch[String]("woele")

extractorMatch[Int](1)
extractorMatch[Integer](1)
extractorMatch[String](1)
extractorMatch[Any](true)
extractorMatch[String]("woele")
}

0 comments on commit 6b3ef4f

Please sign in to comment.