Skip to content

Add partial evaluation of isInstanceOf mini-phase #1275

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

Merged
merged 7 commits into from
May 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Compiler {
new CrossCastAnd, // Normalize selections involving intersection types.
new Splitter), // Expand selections involving union types into conditionals
List(new VCInlineMethods, // Inlines calls to value class methods
new IsInstanceOfEvaluator, // Issues warnings when unreachable statements are present in match/if expressions
new SeqLiterals, // Express vararg arguments as arrays
new InterceptedMethods, // Special handling of `==`, `|=`, `getClass` methods
new Getters, // Replace non-private vals and vars with getter defs (fields are added later)
Expand Down
168 changes: 168 additions & 0 deletions src/dotty/tools/dotc/transform/IsInstanceOfEvaluator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package dotty.tools.dotc
package transform

import dotty.tools.dotc.util.Positions._
import TreeTransforms.{MiniPhaseTransform, TransformerInfo}
import core._
import Contexts.Context, Types._, Constants._, Decorators._, Symbols._
import TypeUtils._, TypeErasure._, Flags._


/** Implements partial evaluation of `sc.isInstanceOf[Sel]` according to:
*
* +-------------+----------------------------+----------------------------+------------------+
* | Sel\sc | trait | class | final class |
* +-------------+----------------------------+----------------------------+------------------+
* | trait | ? | ? | statically known |
* | class | ? | false if classes unrelated | statically known |
* | final class | false if classes unrelated | false if classes unrelated | statically known |
* +-------------+----------------------------+----------------------------+------------------+
*
* This is a generalized solution to raising an error on unreachable match
* cases and warnings on other statically known results of `isInstanceOf`.
*
* Steps taken:
*
* 1. evalTypeApply will establish the matrix and choose the appropriate
* handling for the case:
* 2. a) Sel/sc is a value class or scrutinee is `Any`
* b) handleStaticallyKnown
* c) falseIfUnrelated with `scrutinee <:< selector`
* d) handleFalseUnrelated
* e) leave as is (aka `happens`)
* 3. Rewrite according to step taken in `2`
*/
class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer =>

import dotty.tools.dotc.ast.tpd._

def phaseName = "isInstanceOfEvaluator"
override def transformTypeApply(tree: TypeApply)(implicit ctx: Context, info: TransformerInfo): Tree = {
val defn = ctx.definitions

/** Handles the four cases of statically known `isInstanceOf`s and gives
* the correct warnings, or an error if statically known to be false in
* match
*/
def handleStaticallyKnown(select: Select, scrutinee: Type, selector: Type, inMatch: Boolean, pos: Position): Tree = {
val scrutineeSubSelector = scrutinee <:< selector
if (!scrutineeSubSelector && inMatch) {
ctx.error(
s"this case is unreachable due to `${selector.show}` not being a subclass of `${scrutinee.show}`",
Position(pos.start - 5, pos.end - 5)
)
rewrite(select, to = false)
} else if (!scrutineeSubSelector && !inMatch) {
ctx.warning(
s"this will always yield false since `${scrutinee.show}` is not a subclass of `${selector.show}` (will be optimized away)",
pos
)
rewrite(select, to = false)
} else if (scrutineeSubSelector && !inMatch) {
ctx.warning(
s"this will always yield true if the scrutinee is non-null, since `${scrutinee.show}` is a subclass of `${selector.show}` (will be optimized away)",
pos
)
rewrite(select, to = true)
} else /* if (scrutineeSubSelector && inMatch) */ rewrite(select, to = true)
}

/** Rewrites cases with unrelated types */
def handleFalseUnrelated(select: Select, scrutinee: Type, selector: Type, inMatch: Boolean) =
if (inMatch) {
ctx.error(
s"will never match since `${selector.show}` is not a subclass of `${scrutinee.show}`",
Position(select.pos.start - 5, select.pos.end - 5)
)
rewrite(select, to = false)
} else {
ctx.warning(
s"will always yield false since `${scrutinee.show}` is not a subclass of `${selector.show}`",
select.pos
)
rewrite(select, to = false)
}

/** Rewrites the select to a boolean if `to` is false or if the qualifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is a bit weird rewrite(tree, to = true) does not mean Rewrite tree to true it means Rewrite tree to true unless the type is nullable

* is a value class.
*
* If `to` is set to true and the qualifier is not a primitive, the
* instanceOf is replaced by a null check, since:
*
* `scrutinee.isInstanceOf[Selector]` if `scrutinee eq null`
*/
def rewrite(tree: Select, to: Boolean): Tree =
if (!to || !tree.qualifier.tpe.widen.derivesFrom(defn.AnyRefAlias)) {
val literal = Literal(Constant(to))
if (!isPureExpr(tree.qualifier)) Block(List(tree.qualifier), literal)
else literal
} else
Apply(tree.qualifier.select(defn.Object_ne), List(Literal(Constant(null))))

/** Attempts to rewrite TypeApply to either `scrutinee ne null` or a
* constant
*/
def evalTypeApply(tree: TypeApply): Tree =
if (tree.symbol != defn.Any_isInstanceOf) tree
else tree.fun match {
case s: Select => {
val scrutinee = erasure(s.qualifier.tpe.widen)
val selector = erasure(tree.args.head.tpe.widen)

val scTrait = scrutinee.typeSymbol is Trait
val scClass =
scrutinee.typeSymbol.isClass &&
!(scrutinee.typeSymbol is Trait) &&
!(scrutinee.typeSymbol is Module)

val scClassNonFinal = scClass && !(scrutinee.typeSymbol is Final)
val scFinalClass = scClass && (scrutinee.typeSymbol is Final)

val selTrait = selector.typeSymbol is Trait
val selClass =
selector.typeSymbol.isClass &&
!(selector.typeSymbol is Trait) &&
!(selector.typeSymbol is Module)

val selClassNonFinal = scClass && !(selector.typeSymbol is Final)
val selFinalClass = scClass && (selector.typeSymbol is Final)

// Cases ---------------------------------
val valueClassesOrAny =
ValueClasses.isDerivedValueClass(scrutinee.typeSymbol) ||
ValueClasses.isDerivedValueClass(selector.typeSymbol) ||
scrutinee == defn.ObjectType

val knownStatically = scFinalClass

val falseIfUnrelated =
(scClassNonFinal && selClassNonFinal) ||
(scClassNonFinal && selFinalClass) ||
(scTrait && selFinalClass)

val happens =
(scClassNonFinal && selClassNonFinal) ||
(scTrait && selClassNonFinal) ||
(scTrait && selTrait)

val inMatch = s.qualifier.symbol is Case

if (valueClassesOrAny) tree
else if (knownStatically)
handleStaticallyKnown(s, scrutinee, selector, inMatch, tree.pos)
else if (falseIfUnrelated && scrutinee <:< selector)
// scrutinee is a subtype of the selector, safe to rewrite
rewrite(s, to = true)
else if (falseIfUnrelated && !(selector <:< scrutinee))
// selector and scrutinee are unrelated
handleFalseUnrelated(s, scrutinee, selector, inMatch)
else if (happens) tree
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no difference between happens and !happens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was kept to showcase all cases - but I'll change it to and note this in a comment.

else tree
}

case _ => tree
}

evalTypeApply(tree)
}
}
4 changes: 2 additions & 2 deletions src/dotty/tools/dotc/transform/PatternMatcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans

override def transformTry(tree: tpd.Try)(implicit ctx: Context, info: TransformerInfo): tpd.Tree = {
val selector =
ctx.newSymbol(ctx.owner, ctx.freshName("ex").toTermName, Flags.Synthetic, defn.ThrowableType, coord = tree.pos)
ctx.newSymbol(ctx.owner, ctx.freshName("ex").toTermName, Flags.Synthetic | Flags.Case, defn.ThrowableType, coord = tree.pos)
val sel = Ident(selector.termRef).withPos(tree.pos)
val rethrow = tpd.CaseDef(EmptyTree, EmptyTree, Throw(ref(selector)))
val newCases = tpd.CaseDef(
Expand All @@ -80,7 +80,7 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
// assert(owner ne null); assert(owner ne NoSymbol)
def freshSym(pos: Position, tp: Type = NoType, prefix: String = "x", owner: Symbol = ctx.owner) = {
ctr += 1
ctx.newSymbol(owner, ctx.freshName(prefix + ctr).toTermName, Flags.Synthetic, tp, coord = pos)
ctx.newSymbol(owner, ctx.freshName(prefix + ctr).toTermName, Flags.Synthetic | Flags.Case, tp, coord = pos)
}

def newSynthCaseLabel(name: String, tpe:Type, owner: Symbol = ctx.owner) =
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions tests/neg/i1255.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
object Test {
def foo(x: Option[Int]) = x match {
case Some(_: Double) => true // error
case None => true
}
}
2 changes: 1 addition & 1 deletion tests/pos/t1168.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
object Test extends App {

trait SpecialException {}
trait SpecialException extends Throwable {}

try {
throw new Exception
Expand Down
2 changes: 2 additions & 0 deletions tests/run/isInstanceOf-eval.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
1
2
15 changes: 15 additions & 0 deletions tests/run/isInstanceOf-eval.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
object Test extends dotty.runtime.LegacyApp {
lazy val any = {
println(1)
1: Any
}

any.isInstanceOf[Int]

lazy val int = {
println(2)
2
}

int.isInstanceOf[Int]
}
3 changes: 1 addition & 2 deletions tests/run/t6637.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

object Test extends dotty.runtime.LegacyApp {
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this test deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test List() is evaluated to List[Nothing] which leads to head.isInstanceOf[A with B] will always be true since Nothing is a subtype of A.

The new mini-phase will report an error here, as such the test will fail because it won't compile and println("ok") will never run.

Of course, the mini-phase could just warn here...decide what you think is best and I'll adjust the PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would be better to remove the .isInstanceOf[A with B] part, while keeping the other parts of the test?

class A ; class B ; List().head.isInstanceOf[A with B]
List().head
} catch {
case _ :java.util.NoSuchElementException => println("ok")
}
Expand Down