Skip to content

Support static analysis of required operations #4665

@eernstg

Description

@eernstg

The Java programming language has a try-with-resources feature delivering guarantees for the execution of specific operations on a given object in a specific scope.

try (Connection conn = dataSource.getConnection()) {
  conn.executeQuery("a query");
}

This works like a syntactic transformation when conn has a type which is a subtype of AutoCloseable, adding a finally block where conn.close() is called.

I think it would be useful to have a similar mechanism in Dart.

This issue is a proposal to generalize local variable declarations such that they support a similar kind of guarantee, but somewhat more general, and also somewhat more manually supported.

For example:

class File {
  void close() {}
}

void main() {
  final f = File() do close;
  if (condition) {
    f.close();
  } else {
    ... // Does not invoke `f.close()`.
  }
  if (otherCondition) throw "Whatever";
}

This declaration would give rise to diagnostic messages indicating that f.close is not guaranteed to have been executed at the throw expression, and the same holds for the end of the function body. This outcome is based on a static analysis which is similar to definite assignment analysis for local variables, but it keeps track of whether the statically known member close has been invoked on f. For any location where the function body is exited, and no such invocation of close is guaranteed to have occurred for all control flow paths, the diagnostic is emitted.

The feature is more general than the Java try-with-resources mechanism. It is associated with a local variable declaration rather than a try statement, which means that it needs to be located inside a regular try statement in order to be sound in the case where the function body is exited because of a throwing completion caused by code outside the current function body. However, this is a choice which is made by the developer, and I expect the approach where it is assumed that no such exception will occur is useful as well. So we allow this particular kind of unsoundness to exist, and rely on developers to write the try statement if they need to enforce the given discipline also when the code is exited because of an exception.

Next, the analysis would be able to determine the case where an operation is executed at least once on every control flow path, but it would also be able to determine that it has been executed exactly once, or at most once. It seems useful to make these distinctions, and the feature supports the modifiers once and maybe to handle these variants.

Finally, note that there's no need to connect this mechanism to the lifespan of any given object. In particular, we may wish to keep track of the fact that a particular object will invoke its init method exactly once on all control flow paths, but we don't want to rename init to close in order to satisfy the requirements that the type of the object must be AutoCloseable or anything like that, and we also don't wish to enforce that the object remains local (non-leaked) to the current function body, it's perfectly fine if we enforce that init is called once on a newly created object, and then it's free to be leaked (returned!) to other parts of the program.

Based on these considerations, the feature supports arbitrary lists of identifiers denoting statically known members of the receiver.

void main() {
  final f = File(...);
  try {
    final contents = f.readAsStringSync();
    final thingy = Thingy.parse(contents) do init once;
    L: if (condition) {
      thingy.init(...);
    } else {
      if (otherCondition) break L;
      thingy.init(...);
    }
    // Warning: We may have invoked `thingy.init` zero times.
  } finally {
    f.close(); // Warning at end of function if we forget this.
  }
}

Syntax

<localVariableDeclaration> ::= // Modified rule: Adding one alternative at the end.
    <metadata> <initializedVariableDeclaration> ';'
  | <metadata> <patternVariableDeclaration> ';'
  | <metadata> <doVariableDeclaration> ';'

<doVariableDeclaration> ::=
    'final' <type>? <identifier> ('=' <expression>)? 'do' <doMemberList> ';'

<doMemberList> ::=
    <doMemberElement> (',' <doMemberElement>)*

<doMemberElement> ::=
    <identifier> ('maybe' | 'once')?

Static analysis

The flow analysis is extended with the ability to maintain information about local variables declared as 'do' variables with respect to the declared member names, similar to the information about definite assignment.

A warning is emitted for each point where the current function body is exited, and one of the following conditions is satisfied:

  • A do-variable v has a do-member m with no modifiers, and one or more control flow paths to the current location does not include an invocation of m with v as the receiver.
  • A do-variable v has a do-member m with the modifier once, and one or more control flow paths to the current location does not include an invocation of m with v as the receiver, or it contains more than one such invocation.
  • A do-variable v has a do-member m with the modifier maybe, and one or more control flow paths to the current location contains more than one invocations of m with v as the receiver.

The diagnostic message is emitted for each of these points, but it may be emitted at an earlier location, which may indicate the source of the problem more clearly. For instance, it may be known much earlier that a particular control flow path will executed a once do-variable member twice.

An invocation of a member is an invocation of a method (not a tear-off), and invocation of a getter or a setter. We could include a tear-off of a method as an invocation, and we could require that a getter whose return type is a function type is invoked and the returned function object is also invoked, but we've chosen to include only the most typical kinds of invocations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions