Skip to content

Subtype constraint solving is incomplete, so GADT exhaustiveness checking could be unsound #4029

Open
@Blaisorblade

Description

@Blaisorblade

While trying to explain why #3645 is fixed by #3646, I've found a potential loophole in the proof I've sketched.

Consider checking exhaustiveness of the match in f. Since there's no case for C3, the match is exhaustive if and only if v: Root[Foo] cannot be an instance of C3 <: Root[C].
So, we must check if we can instantiate Foo such that Root[Foo] can be an instance of C3 (that is, if Root[Foo] <:< Root[C]); the match is exhaustive if such an instantiation is impossible. That's the logic of #3646.

However, Dotty's constraint solver is not complete, so it cannot show in general that an instantiation is impossible, as documented in

https://github.com/lampepfl/dotty/blob/2a85106974ce5620eabbf4658810ddb917465145/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L966, https://github.com/lampepfl/dotty/blob/2a85106974ce5620eabbf4658810ddb917465145/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L507

However, to trigger incompleteness, we need the constraint solver to run into a disjunction D1 where both branches give satisfiable constraint, choose the first branch, then run into another constraint which is unsatisfiable under the first branch of D1, but satisfiable under the second branch of D1.

If the issue can be triggered, an "easy" fix would be to have constraint solving detect when it runs into potential incompleteness, and signal that to the caller somehow. But details are tricky.

Action items:

  • try to construct such examples (for me).
  • if found, decide a course of action.

It could get a new checking mode (where we only want to get false if we're sure), or return Option[Boolean] so that if we detect incompleteness we fail. Some details are open, since a success is supposed to also give a substitution usually — luckily, that's not needed for checking missing branches, but it's needed in the converse case, when checking that present branches are reachable; if a branch is reachable we want to refine type variables correctly. Maybe in the incomplete cases we should just not refine type variables at all, to be safe.

/cc @alexknvl @liufengyun .

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions