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 #1828: add warning when patternmatching on generics #1834

Closed
wants to merge 11 commits into from

Conversation

felixmulder
Copy link
Contributor

review by: @smarter

@felixmulder felixmulder added the area:reporting Error reporting including formatting, implicit suggestions, etc label Dec 19, 2016
@felixmulder felixmulder requested a review from smarter December 19, 2016 14:23
else if (knownStatically)
if (valueClassesOrAny) {
if (selector eq defn.ObjectType)
ctx.warning(i"abstract type pattern is unchecked since it is eliminated by erasure", tree.pos)
Copy link
Member

Choose a reason for hiding this comment

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

This should be a Message class

Copy link
Member

Choose a reason for hiding this comment

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

@unchecked should turn the warning off

if (valueClassesOrAny) tree
else if (knownStatically)
if (valueClassesOrAny) {
if (selector eq defn.ObjectType)
Copy link
Member

Choose a reason for hiding this comment

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

Why is this limited to the selector being Object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check the definition of valueClassesOrAny. If the selector is also Object here, we are dealing with a generic type param that's been erased.

We should also check to see that it was not originally Object either.

Copy link
Member

@smarter smarter Dec 19, 2016

Choose a reason for hiding this comment

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

Does that also work with class A; def unsafeCast[S <: A](a: Any) = a match { case s: S => s; case _ => ??? } ? In that case S will be erased to A, not Object.

@smarter
Copy link
Member

smarter commented Dec 19, 2016

We also need warnings when pattern matching on something that will be erased like List[Int]

@felixmulder felixmulder force-pushed the fix-#1828 branch 2 times, most recently from e27232a to 8b7b0ef Compare December 19, 2016 15:55
@felixmulder
Copy link
Contributor Author

@smarter: fixed your suggestions

val selTypeParam = tree.args.head.tpe.widen match {
case AppliedType(tycon, args) =>
ctx.uncheckedWarning(
ErasedType(hl"""|Since type parameters are erased, you should not match on them in
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 one exception to this, and that's arrays.

case xs: Array[Int]

is testable. We should (a) check that it is indeed tested and (b) adapt the error condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! I'll fix this ASAP :)

@retronym
Copy link
Member

A nuance of the Scalac implementation of this is that it allows you to match on List[Int] without warning if the expected type (ie, the scrutinee type) is Seq[Int].

There are a number of bugs that I've got half fixed on a branch, which will at least have some good test cases for you. I'll dig that up when I'm not on my phone.

@felixmulder
Copy link
Contributor Author

@retronym - thanks, that will surely come in handy!

With the nuanced implementation isn't it possible for the user to define a subtype of Seq[Int] upcast it and then match on List[Int]?

i.e:

class Foo extends Seq[Int] { ... }

(new Foo: Seq[Int]) match {
  case xs: List[Int] => ... // no warning
}

@retronym
Copy link
Member

@felixmulder The rationale is that either if the erased instanceof check succeeds, List[Int] is the correct type. Basically, it is a shorthand for saying that the folllowing cast can be statically proven to be sound.

def (x: Seq[Int]) = x match {
  case xs: List[_] is xs.length > 0 => ... x.asInstanceOf[List[Int]].head + 1
}

My WIP to better test and implement our unchecked warnings in in scala/scala#4366

@retronym
Copy link
Member

One more thing: we can also silence the warning with _: List[Int @unchecked]

@felixmulder felixmulder force-pushed the fix-#1828 branch 3 times, most recently from 423e6fb to 2909603 Compare December 22, 2016 11:04
@felixmulder
Copy link
Contributor Author

@odersky - I think this is ready to go in once there's been a final review :)

@felixmulder felixmulder requested a review from smarter January 11, 2017 18:21
@odersky
Copy link
Contributor

odersky commented Jan 29, 2017

The current implementation gives a false warning for

val ss: Seq[Int] = ???
ss match {
  case ss: List[Int] => ???
}

We get:

/Users/odersky/workspace/dotty/tests/neg> dotc match.scala -unchecked -explain
-- [E035] Erased Type Unchecked Warning: match.scala ---------------------------
5 |    case ss: List[Int] => ???
  |                       ^
  |      abstract type pattern is unchecked since it is eliminated by erasure

Explanation
===========
Since type parameters are erased, you should not match on them in match expressions.

This will not be easy to fix with the chosen approach. But it is essential that it is fixed. Otherwise, signalling false negatives will simply mean that people don't play attention to the real problems.

@odersky
Copy link
Contributor

odersky commented Jan 29, 2017

One possible fix might be to refine the translation of the patterm matcher. Here, we currently see:

val ss: scala.collection.Seq[Int] = ???
{
  case val selector11: scala.collection.Seq[Int] = Test.ss
  {
    def case11(): Nothing = 
      {
        def matchFail11(): Nothing = throw new MatchError(selector11)
        if selector11.isInstanceOf[scala.collection.immutable.List[Int]]
           then
         
          {
            case val ss: scala.collection.immutable.List[Int] = 
              selector11.asInstanceOf[
                scala.collection.immutable.List[Int](ss)
              ]
            {
              {
                ???
              }
            }
          }
         else matchFail11()
      }

If the pattern matcher could figure out that the argument is always an Int, it could use

        if selector11.isInstanceOf[scala.collection.immutable.List[_]]

instead of currently:

        if selector11.isInstanceOf[scala.collection.immutable.List[Int]]

@DarkDimius
Copy link
Contributor

DarkDimius commented Jan 29, 2017

@odersky,
I think this false negative can be fixed inside IsInstanceOfEvaluator, without modifying the pattern matcher itself, by observing that List derrives from Seq and that they share the same type argument.
I would prefer pattern matcher to keep the most precise type possible.

@odersky
Copy link
Contributor

odersky commented Jan 30, 2017

Rebased to master

// param is: Any | AnyRef | java.lang.Object
val topType = defn.ObjectType <:< arg
// has @unchecked annotation to suppress warnings
val hasUncheckedAnnot = arg.hasAnnotation(defn.UncheckedAnnot)
Copy link
Contributor

Choose a reason for hiding this comment

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

Two of these conditions seem ad-hoc to me. Why exclude Object and arrays of AnyVal element types? I would suspect these are just symptoms and the real exclusion criteria are wider.

Copy link
Contributor Author

@felixmulder felixmulder Jan 30, 2017

Choose a reason for hiding this comment

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

The anyValArray check is to make sure we allow: xs.isInstanceOf[Array[X]] where X is a primitive. However, before it would have allowed any value class - I constrained it to only allow checking on primitives (199fd9d).

The topType check is to allow explicit checking on top types, eg:

  • xs.isInstanceOf[Array[Any]]
  • xs.isInstanceOf[Array[AnyRef]]
  • xs.isInstanceOf[Array[Object]]

@odersky
Copy link
Contributor

odersky commented Jan 30, 2017

@felixmulder Can you figure out why tests fail now? It seems we have some REPL test failures after rebasing.

@felixmulder felixmulder force-pushed the fix-#1828 branch 2 times, most recently from 6974d2a to 3dd2eb7 Compare January 30, 2017 09:46
@felixmulder
Copy link
Contributor Author

felixmulder commented Jan 30, 2017

The tests were failing because after the rebase, the error number of ErasedType changed. Fixed :)

@odersky
Copy link
Contributor

odersky commented Apr 4, 2017

Needs to be rebased to master. Also need to verify that

val ss: Seq[Int] = ???  
ss match {
  case ss: List[Int] => ???
}

does not give a spurious error. If that's the case LGTM.

@@ -147,7 +185,7 @@ class IsInstanceOfEvaluator extends MiniPhaseTransform { thisTransformer =>

val inMatch = s.qualifier.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.

This is not a correct discrimination for isInstanceOf's used in pattern matching! It would also cover case objects and enum cases. We need a more robust check. Maybe have a special, second isInstanceOf symbol?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@odersky: This logic was proposed by @DarkDimius when I implemented the original IsInstanceOfEvaluator, he could perhaps know of some reason why this isn't a problem. But the basic idea was that:

val bindings created inside a pattern match desugaring would get the case flag. This was so that we can know that they were specifically created by the PatternMatcher.

I'd have to verify that this does indeed conflict with case objects.

If you're sure (or if I get to verifying before you respond), then if I've understood your proposal correctly:

  • PatternMatcher should be changed to emit a special caseIsInstanceOf symbol (or similar)

  • IsInstanceOfEvaluator should intercept these and replace them with either regular isInstanceOf checks or the evaluated expressions.

    should it exclusively do this for the special symbol, or do we still want the optimization to be performed elsewhere?

Last - do you want this fix included in this PR or can it be separate? The PR doesn't otherwise touch the original logic. I'd open a second PR with the implementation during the day so that we don't forget.

Copy link
Contributor

Choose a reason for hiding this comment

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

The caseIsInstanceOf idea looks right (exclusive or not: no idea, propose something :-) It could be a separate PR.

@felixmulder
Copy link
Contributor Author

@odersky - added the missing Seq => List nuance suggested by Jason. If all looks good, please approve and I'll move to fixing the isMatch logic.

@felixmulder
Copy link
Contributor Author

felixmulder commented Apr 7, 2017

@odersky - actually one more caveat. (x: Any) match { case List[_] => ??? } still warns. I'll fix this, then we should be good to go.

Update: fixed in ed5bb8b

@felixmulder
Copy link
Contributor Author

Rebased

Copy link
Contributor

@odersky odersky left a comment

Choose a reason for hiding this comment

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

The general case of false negatives still needs to be solved.

case tr: TypeRef =>
tr.symbol.is(BindDefinedType)
case TypeBounds(lo, hi) =>
(lo eq defn.NothingType) && (hi eq defn.AnyType)
Copy link
Contributor

Choose a reason for hiding this comment

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

eq usually works, but is not in general a reliable method to compare types, since hash consing is not guaranteed. I recommend to use instead:

lo.isRef(defn.NothingClass) && hi.isRef(defn.AnyClass)

@@ -357,6 +357,9 @@ class Definitions {
List(AnyClass.typeRef), EmptyScope)
def SingletonType = SingletonClass.typeRef

lazy val ListType: TypeRef = ctx.requiredClassRef("scala.collection.immutable.List")
def ListClass(implicit ctx: Context) = ListType.symbol.asClass

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 not be needed (see below).

@felixmulder
Copy link
Contributor Author

Needs subtypeWithArgs before progress can be made.

@felixmulder
Copy link
Contributor Author

Since IsInstanceOfEvaluator.scala no longer exists, this PR is obsolete. Issue #1828 still remains to be solved in a different manner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:reporting Error reporting including formatting, implicit suggestions, etc stat:on hold
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants