Skip to content

Exhaustiveness checking says a non-subtype is unhandled. #53486

Open
@lrhn

Description

@lrhn

Example (inspired by from dart-lang/language#3337):

sealed class Result<T, E> {
  const factory Result.value(T value) = ValueResult<T>._;
  const factory Result.error(E error) = ErrorResult<E>._;
}

final class ValueResult<T> implements Result<T, Never> {
  final T value;
  const ValueResult._(this.value);
}

final class ErrorResult<E> implements Result<Never, E> {
  final E error;
  const ErrorResult._(this.error);
}

void main() {
  test(const Result.value(1));
  test(const Result.error("e"));
}

void test(Result<int, String> r, [bool b = false]){
  switch (r) { // Result<int, String>' not exhausted, doesn't match 'ValueResult<dynamic>()'
    case ValueResult<int> v: print(v.value);
    case ErrorResult<String> e: print(e.error);
  }
}

The error message given by both the analyzer and dart2js (using DartPad, master channel) is:

The type 'Result<int, String>' is not exhaustively matched by the switch cases since it doesn't match 'ValueResult<dynamic>()'.

However, ValueResult<dynamic> is not a subtype of the matched element type of Result<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:

sealed class Result<T, E> {
  const factory Result.value(T value) = ValueResult<T, E>._;
  const factory Result.error(E error) = ErrorResult<T, E>._;
}

final class ValueResult<T, E> implements Result<T, E> {
  final T value;
  const ValueResult._(this.value);
}

final class ErrorResult<T, E> implements Result<T, E> {
  final E error;
  const ErrorResult._(this.error);
}

void main() {
  test(const Result.value(1));
  test(const Result.error("e"));
}

void test(Result<int, String> r, [bool b = false]){
  switch (r) {
    case ValueResult<int, String> v: print(v.value);
    case ErrorResult<int, String> e: print(e.error);
  }
}

then the error goes away, so it's something about the Never which makes the exhaustiveness algorithm think that ValueResult<int> doesn't exhaust the ValueResult-subtypes of Result<int, *>.

I can see how ValueResult<int> might only seem to match Result<int, Never>, not all of Result<int, String>, but it exhausts all ValueResults that are actually subtypes of Result<int, *>, no matter the second operand, and exhausting ValueResult<int> by itself should exhaust it as a subtype of Result<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 of Result either, doesn't provoke confidence.

Metadata

Metadata

Assignees

Labels

area-dart-modelFor issues related to conformance to the language spec in the parser, compilers or the CLI analyzer.model-exhaustivenessImplementation of exhaustiveness checking

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions