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

SI-5365 Exhaustivity of extractors, guards, and unsealed traits. #5617

Closed
wants to merge 4 commits into from
Closed

SI-5365 Exhaustivity of extractors, guards, and unsealed traits. #5617

wants to merge 4 commits into from

Conversation

sellout
Copy link
Contributor

@sellout sellout commented Dec 27, 2016

This builds on @retronym’s #4929.

It handles the unsealed trait case (via an additional -Xlint:strict-unsealed-patmat flag) and the extractor-only case (both raised by @durban).

It still defaults to lax checking by default. I don’t know what’s involved in testing it against the community build to see if we can do strict-by-default.

retronym and others added 3 commits February 1, 2016 15:02
Rather than disabling the analysis altogether, rewrite
guard to `true` (by default) or to `false` under a new
compiler option.

The default option errs on the side of avoiding spurious
warnings where the guard writer knows better. However,
there was a convincing argument made in the ticket that
in these cases it would not be to onerous to require
the code to be rewritten from

    case P if g1 =>
    case P if g2 =>

to:

    case P if g1 =>
    case P       =>

Or to:

    (scrut: @unchecked) match {
      case P if g1 =>
      case P if g2 =>

So perhaps we can turn on the strict version by
default, after running against the community build
as a sanity check.

Extractor patterns had the same limitation as guards.
This commit also enables the analysis for them in the
same way as done for guards. However, this still not
done for `unapplySeq` extractors, as the counter example
generator can't handle these yet.
@scala-jenkins scala-jenkins added this to the 2.12.2 milestone Dec 27, 2016
@@ -888,7 +893,7 @@ trait MatchAnalysis extends MatchApproximation {
// if uniqueEqualTo contains more than one symbol of the same domain
// then we can safely ignore these counter examples since we will eventually encounter
// both counter examples separately
case _ if inSameDomain => None
case _ if inSameDomain => Some(WildcardExample)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is probably a better way to handle this case. I’m not sure what should ever hit the previous version of this – no tests failed, but I’m sure I’m overlooking something.

@sellout
Copy link
Contributor Author

sellout commented Dec 27, 2016

I signed the CLA.

@retronym
Copy link
Member

Some tests have changed the output:

From https://scala-ci.typesafe.com/job/scala-2.12.x-validate-test/3948/consoleFull, you can run this to update the expectations ("checkfiles" in partest parlance), and then see if this is as you expect.

# Failed test paths (this command will update checkfiles)
partest --update-check \
  test/files/neg/t1503.scala \
  test/files/neg/t5365.scala \
  test/files/neg/t5365b.scala

@sellout
Copy link
Contributor Author

sellout commented Dec 28, 2016

@retronym Thanks. I must have been running the wrong test command locally, since I missed the failures. Hopefully one of the failures will point me in the right direction for fixing my confusion in toCounterExample.

Handle exhaustivity for `unapplySeq` extractors and restrict a
`WildcardExample` case to when strict pattern matching is enabled.
@sellout
Copy link
Contributor Author

sellout commented Feb 1, 2017

As @durban pointed out on typelevel#129, these changes affect catch blocks as well (if -Xlint:strict-unsealed-patmat is used), but they don’t affect PartialFunction. IMO, a catch block should behave more like a PartialFunction than like an exhaustive pattern match. I’m working to make this change, but if anyone wants to jump in with a pointer, I wouldn’t mind.

@som-snytt
Copy link
Contributor

I'm not following the convo, but I had an old PR to treat catches exactly as PartialFunctions.

#4400

@sellout
Copy link
Contributor Author

sellout commented Feb 1, 2017

@som-snytt Yeah, that sounds like exactly what I want. Should that PR be revived, or should I steal the part I need? (The parsing/hygiene stuff sounds a bit intimidating, so I doubt I’d pick up the whole PR, and @retronym suggested splitting it up anyway).

@milessabin
Copy link
Contributor

FYI: this has been merged in Typelevel Scala 2.12.1.

@sellout
Copy link
Contributor Author

sellout commented Feb 22, 2017

I see there is a failure on the “combined” check. Does that mean I should squash failing commits so that everything in the history builds cleanly?

@SethTisue
Copy link
Member

Does that mean I should squash failing commits so that everything in the history builds cleanly?

yes please. as a rule, before final merge we ask that every commit be green (and that unnecessary merge commits be avoided)

@durban
Copy link

durban commented Feb 26, 2017

Another strangeness (tested with Scalatl 2.12.1, -Xlint:_ and -Xstrict-patmat-analysis):

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> (NoSymbol : Symbol) match {
     |   case NoSymbol => "nosym"
     |   case sym: Symbol if sym.isMethod => "meth"
     |   case x: Symbol => "fallback"
     | }
<console>:15: warning: match may not be exhaustive.
It would fail on the following input: (x: reflect.runtime.universe.Symbol forSome x not in NoSymbol)
       (NoSymbol : Symbol) match {
                 ^
error: No warnings can be incurred under -Xfatal-warnings.

However with a simple trait, it seems to work fine:

scala> trait Foo
defined trait Foo

scala> val x: Foo = new Foo {}
x: Foo = $anon$1@10bd5fb8

scala> (x : Foo) match {
     |   case `x` => "x"
     |   case foo: Foo if scala.util.Random.nextBoolean() => "rnd"
     |   case foo: Foo => "fallback"
     | }
res6: String = x

scala>

@sellout
Copy link
Contributor Author

sellout commented Feb 27, 2017

@durban Thanks. This is an interesting case. It only happens with both flags enabled, unlike the other ones that only required -Xlint:strict-unsealed-patmat.

@sellout
Copy link
Contributor Author

sellout commented Feb 27, 2017

@durban : Symbol uses the universe.SymbolTag.unapply so it’s an extractor, which means it doesn’t help exhastivity. If I swap in : SymbolApi the warning goes away. But why does a type pattern ever use an extractor?

@durban
Copy link

durban commented Feb 27, 2017

@sellout Ok, thanks, that makes (some) sense. So the behavior (while somewhat surprising) seems to be correct. Those cases indeed aren't exhaustive, so the compiler is right.

As to why a (syntactically) type pattern uses an extractor, this comment on ClassTag#unapply seems to be the answer:

  /** 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]`.
   */

So, if I understand correctly, this is to work around type erasure, and make more type patterns work (somewhat) safely. And it seems, that the reflection/macro API uses this feature extensively. Fair enough.

@sellout
Copy link
Contributor Author

sellout commented Feb 27, 2017

@durban Well, your comment just helped me understand it better than the docs I’ve been reading all morning 😆

However, I wonder if we can avoid using the extractor in the case where the type annotation is the same as the scrutinee’s type? Since then you don’t really need to use the unapply. The type doesn’t change – i.e., (x: Q) match { ...; case y: Q => ??? } could be rewritten to (x: Q) match { …; case y => ??? } safely, no?

@sellout
Copy link
Contributor Author

sellout commented Feb 27, 2017

In b4 @paulp explains why that doesn’t work.

@paulp
Copy link
Contributor

paulp commented Feb 27, 2017

@sellout 🤔
screen shot 2017-02-27 at 12 37 30 pm

@sellout
Copy link
Contributor Author

sellout commented Feb 27, 2017

@paulp
Copy link
Contributor

paulp commented Feb 27, 2017

TIL.

@retronym
Copy link
Member

retronym commented Feb 28, 2017

@sellout The typechecker rewrites uncheckable type tests as class tag extractor calls before the pattern matcher reasons about whether a type test is redundant (due to the type of the scrutinee and also the knowledge that previous cases haven't matched). So the pattern matcher just sees an opaque extractor pattern and emits the unapply call.

In other words (inb4 @paulp again; TIAL), "an artifact of the implementation"

@@ -761,6 +765,7 @@ trait MatchAnalysis extends MatchApproximation {
def chop(path: Tree): List[Symbol] = path match {
case Ident(_) => List(path.symbol)
case Select(pre, name) => chop(pre) :+ path.symbol
case Apply(fun, args) => chop(fun) :+ path.symbol
Copy link
Member

Choose a reason for hiding this comment

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

Could you please rebase this change into a separate commit to help understand its impact (ie, is a no-op without the new flag enabled?) and comment this line? I'm assuming we know must account for getting an extractor call in the tree path under the new option.

@@ -56,7 +56,7 @@ class TestSealedExhaustive { // compile only
case Ga =>
}

def ma6() = List(1,2) match { // give up
Copy link
Member

Choose a reason for hiding this comment

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

What change caused this test case to progress?

@durban
Copy link

durban commented Mar 15, 2017

As a sidenote/question: pattern exhaustivity of destructuring vals (e.g., val Some(x) = Option.empty[Int]) doesn't seem to be checked (either with or without this PR). I guess this is because the match this code desugars to, seems to contain an @unchecked annotation. What's the reason for inserting this @unchecked?

@SethTisue SethTisue modified the milestones: 2.12.3, 2.12.2 Mar 21, 2017
@milessabin
Copy link
Contributor

@sellout were you able to make any progress on @durban's issue?

@adriaanm
Copy link
Contributor

adriaanm commented Aug 23, 2017

Happy to reschedule for 2.12.4 once feel we can push this forward in time.

@SethTisue
Copy link
Member

closing for inactivity. anyone want to try and carry this work forward...?

@milessabin
Copy link
Contributor

I'll pick this one up too.

@milessabin milessabin reopened this Feb 27, 2018
@milessabin milessabin self-assigned this Feb 27, 2018
@adriaanm
Copy link
Contributor

adriaanm commented Jun 1, 2018

I'm going to close this one for now. @milessabin: happy to receive your resubmit when ready!

@SethTisue
Copy link
Member

happy ending at #9140

@som-snytt
Copy link
Contributor

Two follow-ups to the thread are -Xlint enables check on "destructuring vals" and the "catch takes a function" is almost in #4400 but I haven't looked at whether checks are applied.

@sellout sellout deleted the ticket/5365 branch December 7, 2022 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
9 participants