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

Missing exhaustivity warning on ints (and other switchable types) #12247

Open
nrinaudo opened this issue Nov 25, 2020 · 8 comments
Open

Missing exhaustivity warning on ints (and other switchable types) #12247

nrinaudo opened this issue Nov 25, 2020 · 8 comments
Labels
Milestone

Comments

@nrinaudo
Copy link

reproduction steps

Scala 2.13.4 is supposed to implement the following:

Guards (case .. if .. =>) no longer disable the exhaustivity checker. Rather, the checker checks for exhaustivity using the "pessimistic" assumption that the guard may be false.

(Source)

Yet, the following code compiles without warning:

def test(i: Int) = i match {
  case value if value < 0 => true
  case value if value > 10 => false
}

This has been tried with and without the -Xlint:strict-unsealed-patmat scalac option.

problem

The way I understand the quoted sentence is: the compiler will consider both branches of the pattern match as potentially being false at the same time, which will cause it to emit a non-exhaustivity warning.

This is not what's happening.

@SethTisue
Copy link
Member

SethTisue commented Nov 25, 2020

agree this is a bug.

a weird thing here is that if you add another clause, the checking comes back!

scala 2.13.4> def test(i: Int) = i match {
            |   case value if value < 0 => true
            |   case value if value > 10 => false
            |   case 5 => true
            | }
                                 ^
              warning: match may not be exhaustive.

@SethTisue SethTisue added this to the 2.13.5 milestone Nov 25, 2020
@dwijnand
Copy link
Member

So -Vpatmat is the flag to get some pattern matching output, and normally it's an overwhelming amount of noise (that took me a few weeks to grok and be comfortable with) but very unexpectedly this code shows close to none!

$ m Ints.scala
class Test {
  def test(i: Int) = i match {
    case value if value < 0 => true
    case value if value > 10 => false
  }
}
$ scalac -Vpatmat Ints.scala
translating {case (value @ _) if value.<(0) => true
case (value @ _) if value.>(10) => false}
combining cases: {G(value.<(0)) >> B(true,Boolean)
G(value.>(10)) >> B(false,Boolean)}
def: (case <synthetic> val x1: Int = i,List(value x1),List(method test, class Test, package <empty>, package <root>))

Why is that? Because it can be emitted by a switch, so it doesn't do any analysis, because analysis was for those non-switchable sealed types! I'll try to fix this for the next release. I'll also try to suss out what it is about case 5 that changes that, as I think that should be switchable too (a separate bug, I guess).

@dwijnand
Copy link
Member

dwijnand commented Jan 4, 2021

I have a branch where I got this working. But making switchable types emit warnings fixes this and then highlights all the times (e.g. in the compiler) where internal implementations are switching on ints because of how fast it is, for the (let's say) 6 ints that are tracked internally. I think I ran out of steam/patience to fix all the cases in the compiler before I gave myself a break and considered that this might be just bad idea...

@dwijnand dwijnand changed the title Missing exhaustivity warning in patmatch Missing exhaustivity warning on ints (and other switchable types) Jan 4, 2021
@martijnhoekstra
Copy link

could something like an annotation @openswitch that combines the functionality of @switch and @unchecked be useful there. This got me thinking about the two kinds of unchecked matches:

  • The match is exhaustive in the domain of the scrutinee, but scalac doesn't know it: The programmer knows more than the compiler about the generators. @unchecked fits this case perfectly: you tell scalac it's ok it can't check exhaustiveness, and the reader that scalac isn't going to help you check exhaustiveness. This is the "not known to be exhaustive" variety.

  • The domain of the scrutinee is smaller than the domain of the static type of the scrutinee (example: HTTP Status code represented as Short). You accept as a programmer that not all values of the type of the scrutinee are represented in the match. This is the "know to be not exhaustive" variety -- which is the one that I initially expected to be less of a problem to warn for. In this case the programmer knows more about the domain of some value than the compiler. @unchecked sounds like the wrong name for communicating this, and feels wrong to use.

In the case of switch matches, you commonly want to use a type that allows for more values than your domain, because you're restricted by performance considerations. Maybe it makes sense to combine the annotation for not caring about non-exhaustiveness and the annotation for caring about the switch performance.

@dwijnand
Copy link
Member

dwijnand commented Jan 7, 2021

Can we reuse @switch? Two questions:

  1. Are there legit cases where I @switch an int and I want to get exhaustivity checks?
  2. Are there legit cases where I don't @switch an int and I don't want to get exhaustivity checks?

(I say int but I mean any switchable type: byte, short, int, char, String.)

I think the second one is easier: if you're not using a switch table and you've got an incomplete match, then @unchecked it. But is the first one ok? 🤔

@martijnhoekstra
Copy link

Figuring out what I mean exactly: I always want exhaustiveness checks on the domain of the match, but I can't always communicate the domain to the compiler. Even if I can't, I may still want the compiler to warn you that you don't have a default case. But maybe that's uncommon enough to either not support it, or to support it with a new opt-in annotation.

As for concrete answers to your questions:

  1. Yes, you may to want a switch with a default case, especially (but not only) when you're switching on actual ints as numbers, not as labels representing something else. Inspired by a recent actual scenario, I can imagine:
def rep(p: Parser[A], max: Int, fact: Factory[A, B]): ParserB = (max @switch) match {
  case 0 => Parser.pure(fact.newBuilder().result)
  case 1 => p.map(a => fact.newBuilder().addOne(a).result()).?.map(_.getOrElse(fact.newBuilder().result))
  case n => Repeater(p, n -1, fact)
}
  1. Yes. For example, I can easily imagine a match on a HTTP status code represented as an Int or short, matching stuff like
status match {
  case info if info >= 100 && info <  200 =>
  case 200 =>
  case ok if ok > 200 && ok < 300 =>
  case 400 =>
  case 401 =>
  case 403 => 
  case agentError if agentError >= 400 && agentError  < 500 =>
  case serverError if serverError >= 500 =>
}

For the second maybe (status @unchecked) match is good enough, and outside the scope of this issue.

For the first, maybe it's enough to suck it up and don't have exhaustiveness checking even though you would prefer it, maybe introduce, I don't know, (max @checkedswitch) match

@SethTisue SethTisue modified the milestones: 2.13.5, 2.13.6 Feb 9, 2021
@dwijnand dwijnand modified the milestones: 2.13.6, 2.13.7 May 10, 2021
@SethTisue SethTisue modified the milestones: 2.13.7, 2.13.8 Oct 21, 2021
@dwijnand dwijnand modified the milestones: 2.13.8, Backlog Nov 18, 2021
@dwijnand dwijnand removed their assignment Nov 18, 2021
@SethTisue
Copy link
Member

related: #12770

@som-snytt
Copy link

Do my expectations (for a warning if it can prove inexhaustivity) change if the expected type is a partial function?

Also, I haven't kept up with the latest on Dotty, but probably innovation must start there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants