-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Exhaustiveness checking says a non-subtype is unhandled. #53486
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
Ah, yeah. I've had this in the back of my mind for a while. Take a look at this part of the exhaustiveness spec: To expand a space
In cases where the relationship between a sealed supertype and its subtypes isn't just simple forwarding, the algorithm kind of gives up and takes a simpler conservative approach ("over-approximation"). @johnniwinther figured this all out on his own when implementing it since I didn't have anything approaching a complete design at the time (and what I had didn't cover generics at all). The approach is much better than I would have been able to come up with, but there might still be room to make it a little more sophisticated in cases like this. At the time, I figured sealed classes hierarchies that (a) were also generic and (b) used the type parameters in non-simple-forwarding ways would be so obscure and rare as to not be worth worrying about. Unfortunately, I didn't realize that the obvious way to implement Option and Either types—probably the most common use of sum types!—happens to fall right into that set. :( |
The real solution is to perform a constraint-solving. Given that I couldn't share that code between the analyzer and CFE, I chose the approximation to be able to implement a working solution in time for the feature release. |
Let me see if I can read this. Take: sealed class Result<R, E> {}
final class ValueResult<R> implements Result<R, Never> {}
final class ErrorResult<E> implements Result<Never, E> {}
void main() {
Result<int, Error> r = null as dynamic; // Only here for the static errors.
switch (r) { case ValueResult<int> _: ...; case ErrorResult<Error> _: ...}
} When switching on (the type T)
Those spaces are, I think, the ones to exhaust in order to exhaust T. And to solve that, we do need some kind of constraint solving, to know for which sealed class Json<T> {
T get value;
}
abstract final class JsonList<T> extends Json<List<T>> {
final List<T> value;
T operator[](int index) => value[index];
}
abstract final class JsonMap<T> extends Json<Map<String, T>> {
final Map<String, T> value;
T? operator[](String key) => value[key];
}
abstract final class JsonLiteral<T> extends Json<T> {
final T value;
} where we might need to solve |
Example (inspired by from dart-lang/language#3337):
The error message given by both the analyzer and dart2js (using DartPad, master channel) is:
However,
ValueResult<dynamic>
is not a subtype of the matched element type ofResult<int, String>
, so that type should not need to be matched.(If I do match it, I get told to also match
ErrorResult<dynamic>
.)If I forward both type arguments to the subclasses:
then the error goes away, so it's something about the
Never
which makes the exhaustiveness algorithm think thatValueResult<int>
doesn't exhaust theValueResult
-subtypes ofResult<int, *>
.I can see how
ValueResult<int>
might only seem to matchResult<int, Never>
, not all ofResult<int, String>
, but it exhausts allValueResult
s that are actually subtypes ofResult<int, *>
, no matter the second operand, and exhaustingValueResult<int>
by itself should exhaust it as a subtype ofResult<int, *>
too.Maybe it's the "spaces" computation which gets confused.
In any case, saying that I should match
ValueResult<dynamic>
, which is not related to the second type argument ofResult
either, doesn't provoke confidence.The text was updated successfully, but these errors were encountered: