Skip to content
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

Fix #3324: add isInstanceOf check #4045

Merged
merged 30 commits into from
Apr 5, 2018
Merged

Conversation

liufengyun
Copy link
Contributor

@liufengyun liufengyun commented Feb 27, 2018

Fix #3324: add isInstanceOf check.

This specification is different from the scalac compiler implementation. I hope it's more understandable.

enum Result[+T, +E] {
case OK [T](x: T) extends Result[T, Nothing]
case Err[E](e: E) extends Result[Nothing, E]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add a comment why this test?

@@ -152,7 +153,7 @@ class SyntheticMethods(thisPhase: DenotTransformer) {
* def equals(that: Any): Boolean =
* (this eq that) || {
* that match {
* case x$0 @ (_: C) => this.x == this$0.x && this.y == x$0.y
* case x$0 @ (_: C @unchecked) => this.x == this$0.x && this.y == x$0.y
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add comment warning maintainers this code is fragile, since unchecked might hide type errors...

@Blaisorblade Blaisorblade self-assigned this Mar 5, 2018
def eval[T](e: Exp[T]) = e match {
case Fun(x: Fun[Int, Double]) => ??? // error
case Fun(x: Exp[Int => String]) => ??? // error
}
Copy link
Contributor

@Blaisorblade Blaisorblade Mar 6, 2018

Choose a reason for hiding this comment

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

Looking at this with @liufengyun, we realize this complication has to do with compiling a match like:

def eval[T](e: Exp[T]) = e match {
  case f @ Fun(x: Fun[Int, Double]) => ???
}

The correct desugaring is a bit surprising to me, because we want to ensure f: Fun[Int, Double], but this is not justified where f is bound but only after the inner match.

def eval[T](e: Exp[T]) = e match {
  case f1: Fun[a1, b1] =>
    f1.f match {
      case f2: Fun[Int, Double] =>
        //***Here*** we learn that a1 = Int and b1 = Double
        //So only ***here*** we can bind `f` with the correct type.
        val f: Fun[Int, Double] = f1
    }
}

We suspect the typechecker currently refines type variables a1 and b1 too soon, which has been fine until now, but strictly speaking means (I think) the current typechecker output is not well-typed in a sound typesystem.

EDIT: as usual, up to misunderstandings.

@smarter
Copy link
Member

smarter commented Mar 6, 2018

Haven't looked at what this PR does in details, but one thing to note is that right now the GADT bounds are stored separately in typer and not preserved in subsequent phases. There used to be code in PostTyper to do this that got removed (I don't know why), you may need to bring it back: eb21d24#diff-21d08d8a179119a483e67f9f0f994e99L62

@liufengyun liufengyun force-pushed the fix-3324 branch 2 times, most recently from 6a14322 to 4bdcaa8 Compare March 7, 2018 08:38
@liufengyun
Copy link
Contributor Author

Thanks for the tip @smarter . Upon reflection, it seems to correctly handle GADTs in patterns, we need some context-sensitive typing, the type of a term name or type name could be refined in a context depending on type test. The refinement should be in sync with the type test.

In TypeScript, there's a related feature called type guards, which can refine the type of a variable in a context:

// Type is 'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // type narrowed to 'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // type narrowed to 'StringPadder'
}

I've no idea how this works in theory. In our case, we need to refine the type of types names (as @Blaisorblade points out in the example) instead of / (in addition to) term names.

@liufengyun
Copy link
Contributor Author

An example related to GADT typing of Dotty trees:

class Test {
  trait Type
  class Tree[-T]
  case class Ident[-T](x: String) extends Tree[T]

  trait Base extends Tree[Type]
  class DummyIdent extends Ident[Null]("hello") with Base

  def foo(tree: Tree[Type]): Ident[Type] = tree match {
    case id @ Ident(x) => id                   // should we emit warnings here?
  }

  foo(new DummyIdent)
}

@Blaisorblade
Copy link
Contributor

@liufengyun DummyIdent should be forbidden by the PR for #3989 — as I said, I'd wait for that PR before trying to get right variant GADTs.

I've no idea how this works in theory

TypeScript has flow-sensitive typing; in Haskell's internal language used for GADTs, IIUC, matches produce evidence that some type is equal or smaller than another (=:= and <:<). Doing that properly would require adapting the theory though, which isn't an immediate step.

@Blaisorblade
Copy link
Contributor

I just confirmed that scalac's established behavior with isInstanceOf and ClassTag is likely incorrect:

https://gist.github.com/Blaisorblade/a0eebb6a4f35344e48c4c60dc2a14ce6

@Blaisorblade
Copy link
Contributor

And people pointed me to scala/bug#9565 and https://groups.google.com/d/msg/scala-internals/KYZACmy8_Qg/Wh1T0coqBYYJ.

For this PR, I suspect ClassTag-based isInstanceOf should just warn iff we are not in Scala2 mode (to be compatible in Scala2 mode and sound in Dotty mode), since picking a fix will take a while.

@liufengyun
Copy link
Contributor Author

@Blaisorblade If we always warn for a ClassTag-based test like x: T, it makes this feature not usable. (BTW, ClassTag will not generate isInstanceOf, but just a call to uanpply on the evidence)

I think soundness is not the only concern here. Giving that it happens rarely, I think we should optimize for usability as Scalac does.

@smarter
Copy link
Member

smarter commented Mar 8, 2018

@Blaisorblade If we always warn for a ClassTag-based test like x: T, it makes this feature not usable.

It's not great, but one can always use @unchecked to suppress the warning.

@@ -12,7 +12,7 @@ class Test {
}

def quux[T](a: A[T]): Unit = a match {
case _: B[T] => // error
case _: B[T] => // err-or: cannot handle this for now
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe replace err-or with should be an error or such?

@liufengyun
Copy link
Contributor Author

For (x: X).isInstanceOf[P], scalac emits an unchecked warning when:

We designate another type, XR, which is essentially the intersection of X and |P|, where |P| is the erasure of P. If XR <: P, then no warning is emitted.

ref: https://github.com/scala/scala/blob/94cee79eb63b4449d21dc74cb9175d6412cc8a55/src/compiler/scala/tools/nsc/typechecker/Checkable.scala#L12-L64

However, the above specification cannot explain why we should not produce a warning in the following case:

sealed trait A[T]
class B[T] extends A[T]

def g(x: A[Int]) = x match { case _: B[Int] => }

The specification in this PR can be seen as making the obscurity in Scalac specification clear by resorting to type inference. Otherwise, the two are equivalent in spirit.

@allanrenucci
Copy link
Contributor

test performance please

@dottybot
Copy link
Member

performance test scheduled: 1 job(s) in queue, 0 running.

Copy link
Contributor

@allanrenucci allanrenucci left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM

case fn: Select if fn.symbol == defn.Any_typeTest =>
ensureCheckable(fn.qualifier, tree.args.head)
case fn: Select if fn.symbol == defn.Any_isInstanceOf =>
ensureCheckable(fn.qualifier, tree.args.head)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not merge the first two cases:

case fn: Select if fn.symbol == defn.Any_typeTest || fn.symbol == defn.Any_isInstanceOf =>
  ensureCheckable(fn.qualifier, tree.args.head)

def replaceP(implicit ctx: Context) = new TypeMap {
def apply(tp: Type) = tp match {
case tref: TypeRef
if !tref.typeSymbol.isClass && tref.symbol.is(Case) => WildcardType
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain what this test does?

* 1. if `X <:< P`, TRUE
* 2. if `P` is a singleton type, TRUE
* 3. if `P` refers to an abstract type member or type parameter, FALSE
* 4. if `P = Array[T]`, checkable(E, T) where `E` is the element type of `X`, defaults to `Any`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you explain why there is a special treatment for Array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @allanrenucci for the helpful feedback, I've addressed them in the latest commit.

For Array, we need a special treatment, because JVM carries type information of array elements, it's possible to test the element type at runtime [1]:

scala> Array(1, 2, 3).getClass
res2: Class[_ <: Array[Int]] = class [I
// where the string "[I" is the run-time type signature for the class 
// object "array with component type int".

[1]: https://docs.oracle.com/javase/specs/jls/se7/html/jls-10.html

* (a) replace `Ts` with fresh type variables `Xs`
* (b) constrain `Xs` with `pre.F[Xs] <:< X`
* (c) instantiate Xs and check `pre.F[Xs] <:< P`
* 6. if `P = T1 | T2` or `P = T1 & T2`, checkable(X, T1) && checkable(X, T2).
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a test for & types. For example:

trait Marker
def foo[T](x: T) = x match {
  case _: T & Marker => // no warning
  // case _: T with Marker => // scalac emits a warning
  case _ =>
}

Scalac emits a warning but I believe it is erroneous.
Use case:

def foo[T](xs: List[T]): List[T] = xs.collect { case x: T with Marker => x }

def replaceX(implicit ctx: Context) = new TypeMap {
def apply(tp: Type) = tp match {
case tref: TypeRef
if !tref.typeSymbol.isClass && tref.symbol.is(Case) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be isPatternTypeSymbol(tref.typeSymbol)

trait Marker
def foo[T](x: T) = x match {
case _: (T & Marker) => // no warning
// case _: T with Marker => // scalac emits a warning
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably tested too

case _ =>
}

def bar(x: Any) = x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

You could remove this line and it pos test. This already tested in i3324.scala

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the catch. This line is intended to make the test a neg test. Make it a pos test will not work, as we want to check warnings here, -fatal-warning is required. And it's nice to have all related tests of a feature under one folder.


def g(x: A[Int]) = x match { case _: B[Int] => }

def foo(x: Any) = x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

}

def foo(x: Any): Boolean =
x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

}

def foo(x: Any): Boolean =
x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

// test parametric case classes, which synthesis `canEqual` and `equals`
enum Result[+T, +E] {
case OK [T](x: T) extends Result[T, Nothing]
case Err[E](e: E) extends Result[Nothing, E]
Copy link
Contributor

Choose a reason for hiding this comment

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

What is tested here? There is no pattern match

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here the synthesized code will generate pattern match in canEqual and equals.

}

def foo(x: Any): Boolean =
x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

eval(exp)(Env.empty)

def foo(x: Any): Boolean =
x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

}

def foo(x: Any): Boolean =
x.isInstanceOf[List[String]] // error
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this line. Tested in i3324.scala?

@liufengyun
Copy link
Contributor Author

Merge now, so that we can fix bugs related to this feature.

@liufengyun liufengyun merged commit b1d9162 into scala:master Apr 5, 2018
@liufengyun liufengyun deleted the fix-3324 branch April 5, 2018 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants