Skip to content

Consider expanding flow analysis' ability to prove that a pattern "always matches" #2980

Closed
@stereotype441

Description

@stereotype441

Today I spent some time writing some toy analysis code using patterns. I was building up a switch statement on an as-needed basis, so I had a default clause to handle the cases I hadn't written yet. The code looked like this:

  ir.CodeReference lowerElement(ExecutableElement element) {
    switch (element.enclosingElement) {
      case ClassElement class_:
        return ir.ClassMemberReference(
            Uri.parse(element.library.definingCompilationUnit.uri!),
            class_.name,
            element.name);
      default:
        throw 'TODO(paulberry): ${element.enclosingElement.runtimeType}';
    }
  }

Then I got the bright idea that instead of a default clause, I ought to be able to use an object pattern, i.e.:

  ir.CodeReference lowerElement(ExecutableElement element) { // (1)
    switch (element.enclosingElement) {
      case ClassElement class_:
        return ir.ClassMemberReference(
            Uri.parse(element.library.definingCompilationUnit.uri!),
            class_.name,
            element.name);
      case Object(:var runtimeType):
        throw 'TODO(paulberry): $runtimeType';
    }
  }

But this didn't work. The analyzer produced an error at (1): "The body might complete normally, causing 'null' to be returned, but the return type, 'CodeReference', is a potentially non-nullable type." But in truth, the switch statement is exhaustive (because the type of element.enclosingElement is non-nullable). Flow analysis just wasn't smart enough to see that it was.

In the general case, it's not possible for flow analysis to be as smart as the exhaustiveness algorithm. This is a known limitation of the design of the compilation pipeline, and we have discussed it elsewhere (for example, dart-lang/sdk#51926 (comment)).

However, flow analysis does understand that variable and wildcard patterns might always match (because the type of the pattern is a supertype of the matched value type), and in those cases it considers the switch to be exhaustive. I think it would be worth expanding the set of patterns that flow analysis recognizes as "always matching" to include:

  • A cast pattern, if the subpattern is always matching
  • A list pattern of the form [...], if the required type is a supertype of the matched value type
  • A logical and pattern, if both subpatterns are always matching
  • A logical or pattern, if either of the subpatterns is always matching
  • A null check pattern, if the matched value type is non-nullable and the subpattern is always matching
  • A null assert pattern, if the subpattern is always matching
  • An object pattern, if the required type is a supertype of the matched value type and all subpatterns are always matching
  • A record pattern, if the required type is a supertype of the matched value type and all subpatterns are always matching

That would allow the example above to work. It would also reduce the number of situations where flow analysis and the exhaustiveness algorithm disagree about whether a switch is exhaustive, which should reduce user pain (see #2977).

(Note that if we were to implement this functionality prior to removing support for mixed-mode null safety, we would have to be careful to make sure that this didn't lead to unsoundness escalation (see #1143).

Metadata

Metadata

Assignees

No one assigned

    Labels

    flow-analysisDiscussions about possible future improvements to flow analysispatternsIssues related to pattern matching.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions