Trouble with 2.13.4 exhaustivity checking being too strict

Hi there,

I’m trying to migrate some of our company’s projects to Scala 2.13.4 and I’m hitting some problems with the new exhaustivity checking algorithm. While this is obviously a very good change, it becomes a problem in some cases where the compiler cannot infer that a type is effectively sealed.

For example, we are using Opt - a value class version of Option. Its usage looks like this:

val someString: Opt[String] = Opt("foo")
val noneString: Opt[String] = Opt.Empty

someString match {
  case Opt(str) => println(s"some: $str")
  case Opt.Empty => println("none")
}

Unfortunately, the compiler doesn’t know that Opt.apply and Opt.Empty are the only possible cases and thinks that this pattern match is non exhaustive. Since Opt is an unsealed type (as seen by the compiler), I can work around that problem by opting out of strict-unsealed-patmat.

However, the problem resurfaces when Opt is wrapped into a sealed type, e.g. Try:

val tryOpt: Try[Opt[String]] = Success(Opt("foo"))

tryOpt match {
  case Success(Opt(str)) => println(s"some $str")
  case Success(Opt.Empty) => println("none")
  case Failure(cause) => cause.printStackTrace()
}

Even with the strict-unsealed-patmat turned off, the compiler complains that:

match may not be exhaustive.
It would fail on the following input: Success((x: com.avsystem.commons.misc.Opt[?] forSome x not in Empty))

Another example is with our enum implementation, ValueEnum:

final class Weekday(implicit enumCtx: EnumCtx) extends AbstractValueEnum
object Weekday extends AbstractValueEnumCompanion[Weekday] {
  final val Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday: Value = new Weekday
}

val tryWeekday: Try[Weekday] = Success(Weekday.Monday)

tryWeekday match {
  case Success(Weekday.Monday | Weekday.Tuesday | Weekday.Wednesday) => println("first half")
  case Success(Weekday.Thursday) => println("middle")
  case Success(Weekday.Friday | Weekday.Saturday | Weekday.Sunday) => println("second half")
  case Failure(cause) => cause.printStackTrace()
}

The compiler has no idea that Monday, Tuesday, etc. are all possible values for Weekday - this is guaranteed by the ValueEnum machinery. So this time I get this warning:

match may not be exhaustive.
It would fail on the following input: Success((x: Weekday forSome x not in (Friday, Monday, Saturday, Sunday, Thursday, Tuesday, Wednesday)))

What are the possible solutions? There are some that come to my mind:

  1. Opting out of strict-unsealed-patmat should also work when the unsealed type is wrapped in a sealed type (like Try[Weekday])
  2. There should be a way to explicitly exempt some types from exhaustivity checking, e.g. with an annotation.
  3. There should be a way to tell the compiler about possible values and constructors of an effectively sealed type. Even if this was as low level as some API available to compiler plugins, I would be happy.

The no. 3 option would be absolutely the best because this would allow full integration of types like Opt into the new exhaustivity checking algorithm.

2 Likes

Doesn’t ”@unchecked” work?–

I does but I don’t consider it a solution. In case of Opt, I’d ideally like to use it as a drop-in replacement for Option, without additional ceremony.

Also, I don’t want to spark a discussion about Opt itself and whether its worth/justified to introduce types like this. It’s just an example of a more general problem.

In this case, dropping in an unsealed type in place of a sealed type will not be a drop-in replacement.–

1 Like

Proper exhaustivity checking is the only feature that it’s missing as compared to Option. With Scala 2.13.4 this became a bigger problem because lack of warnings has turned into false warnings. As a result, I can’t comfortably upgrade to 2.13.4.

For these reasons, I proposed solutions that would both solve my immediate problem and would potentially be useful in general. I want to know if there is some more general interest in what I proposed or at least if someone can help me with the immediate trouble that I face. If not, I will have no choice but to ditch types like Opt. This is a last resort for me and I would be really unhappy if I had to do it because it’s a lot of work and saying goodbye to some nice features that I’ve been relying on for the last few years.

3 Likes

-Wconf is the mechanism to “silence” rando warnings. It can match a warning by message, category, site.

scalac -Wconf:help is the new one-question FAQ.

Thanks, that might indeed be sufficient with Opt but unfortunately not with enums because they are created left and right and can’t all be covered with a single message pattern.

EDIT: I spoke too soon. I have cases where the warning message related to Opt looks like this:

match may not be exhaustive.
It would fail on the following input: Success((_, _))

How do I suppress this without suppressing practically every exhaustive checking warning?

Unsealed patmat is a red herring: Opt is not unsealed at all. In fact, it’s final. The problem is that the extractor Opt.unapply can’t be proven to be exhaustive in combination with Opt.Empty.

To get the compiler to assume your guards and extractors are exhaustive like it was before, you can get the old behaviour where the compiler assumes guards and extractors are true back by setting -Xnon-strict-patmat-analysis.

2 Likes

If it’s not unsealed then why disabling strict-unsealed-patmat suppresses the warning in the code below?

val someString: Opt[String] = Opt("foo")
someString match {
  case Opt(str) =>
  case Opt.Empty =>
}

Thanks for the -Xnon-strict-patmat-analysis option, it should be enough for now but it’s sad that I have to disable the new algorithm entirely.

Looking at the implementation, the compiler does not consider final classes sealed.

I filed an issue related to this discussion.

2 Likes

That’s a good question. It totally shouldn’t.

Then it starts warning for other classes like String and Int…

It should warn for mutually exhaustive extractors on Int or on String.

A hypothetical

(i: Int) match {
  case Zero => ...
  case NonZero(nz) => ...
}

Should not be affected by unsealed-patmat either, should it? As far as I can see, that’s entirely equivalent.

I think it would definitely be valuable if there was a mechanism to indicate to the compiler that a set of extractors together are exhaustive. I.e. Opt and Opt.Empty, or Zero and NonZero.

4 Likes

I made a proposal #11186, which might potentially solve the problem.

6 Likes