-
Notifications
You must be signed in to change notification settings - Fork 214
Generalize exhaustiveness checking for non-trivial generics #4286
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
Comments
Here's an idea about how this could be done: Checking the example here and in #3633, #3866, and #4199, it looks like we're only talking about the situation where the scrutinee type is sealed. It seems likely that we could have similar considerations for For simplicity, we don't consider the possibility that type parameters could be contravariant or invariant (the approach would need to be generalized a bit in order to handle that, but it should be doable). The basic idea is that for a switch whose scrutinee type Assume the following hierarchy, where the kind of the subtype relationship from the sealed type to each of its immediate subtypes can vary freely (that is, it could be sealed class A<X1 extends Bx1 .. Xk extends Bxk> .../*superinterfaces: not important*/... {}
class B1<Y1 extends By1 .. Yn extends Byn> extends A<T1 .. Tk> {}
class B2<Z1 extends Bz1 .. Zm extends Bzm> extends A<S1 .. Sk> {} Consider the following switch expression: int example(A<U1 .. Uk> a) => switch (a) {
B1<V1 .. Vn>() => 0,
B2<W1 .. Wm>() => 0,
}; In order to decide whether or not this switch expression is exhaustive, we could do the following: Perform the matching For all type variables Otherwise (when the match succeeds), we know that any binding of The same approach determines whether Because of the definition of It is worth noting that the match is kind of upside down, in the sense that we make sure the pattern @johnniwinther, WDYT? [Correction, Mar 18: Add the constraint that we don't match |
Let me try to unfold the idea I mentioned on the concrete example: sealed class A<X, Y> {}
class B1<X> implements A<X, Never> {}
class B2<Y> implements A<Never, Y> {}
int f(A<String, Exception> a) => switch (a) {
B1<String>() => 1,
B2<Exception>() => 2
}; We have exactly one case for We need to match So the set of all constraints is This implies that if Hence, the switch is exhaustive. If we consider Note that this is not a complete solution (we don't have any guarantees that such a solution exists, or that it can be built using matching). One case which is not covered is the case where some type arguments are involved, but the subtype still restricts the possible actual type arguments to a proper subset: sealed class C<X, Y> {}
class D1<X, Z> extends C<X, List<Z>> {}
class D2<U extends num, Y> extends C<List<U>, Y> {}
int g(C<Iterable<Comparable<num>>, Iterable<String>> c) => switch (c) {
D1<Iterable<Object>, String>() => 1,
D2<num, Iterable<String>>() => 2,
}; For This means that we can't use this technique when the dependency structure among the type variables and superinterfaces is too complex, unless we generalize the approach with this recursive step. For Inserting the actual type arguments of the cases we get |
Here's another perspective on the task. It's directly derived from the mathematical structure of the task which means that it doesn't say much about a solution. However, it can still be useful as a backstop: If you think you have a solution then you can use this as a sanity check: Every solution must belong to the set of solutions that are implied by this characterization: For any given scrutinee For each immediate subtype of (Note that For each The switch is exhaustive if and only if this subset relation holds. The algorithm I've hinted at in the previous comments covers some special cases of this relation (in particular, I assumed exactly one case per subtype). Another important special case arises when there is a case for Conversely, if there is an element |
Uh oh!
There was an error while loading. Please reload this page.
See dart-lang/sdk#60260 for some background. Thanks to @TekExplorer for bringing this up!
Dart's exhaustiveness analysis recognizes many situations where a switch is guaranteed to cover all possible values of the scrutinee, including having a catch-all case at the end, covering all the values of an enumerated type, or covering each of the classes that are the immediate subtypes of a
sealed
class.However, the current analysis only covers a few special cases when it comes to type arguments. For example:
The only situations that may occur at run time are the following:
a
is aB1<T>
for someT
which is a subtype ofString
.a
is aB2<S>
for someS
which is a subtype ofException
.This means that the switch is actually exhaustive, but the exhaustiveness analysis is not able to prove it. This issue serves as a reminder that we might be able to generalize the current exhaustiveness analysis in order to cover more cases.
The text was updated successfully, but these errors were encountered: