dart-lang / dart-lang/language

Inconsistent inferred return type of function literal with specialized body

Open
#3,715 1 comment 0 reactions 0 assignees View on GitHub
question type-inference
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

Consider the following program:

```dart
// Example 1.

void f(void Function() g) => print(g.runtimeType);

void main() {
f(() {}..expectStaticType>());
f(() sync* {}..expectStaticType>());
f(() async {}..expectStaticType>());
f(() async* {}..expectStaticType>());
}

typedef Exactly = X Function(X);

extension on X {
X expectStaticType>() => this;
}
```

This program has an implied expectation that the inferred return type of the function literal is `void` in all four cases (if it isn't `void` or some other top type then we'll have a compile-time error).

The reason why the function literal return type should be `void` is the following rule from [this section](https://github.com/dart-lang/language/blob/main/resources/type-system/inference.md#function-literal-return-type-inference):

> if `R` is `void`, or the function literal is marked `async` and `R` is `FutureOr`, let `S` be `void`

In the context, `R` is the 'imposed return type schema' (which is obtained from the context type of the function literal) and `S` is the inferred return type of the function literal. In the example above `R` is `void`, and we're computing the value of `S`. The rule cited above says that `S` should be `void`. This result is applicable to all kinds of function literals (and we don't have a context type whose return type is `FutureOr` and hence we can ignore the part between the commas).

The motivation for this rule is that this will allow function literals to have the same kind of `void` related sanity checks that a regular function gets. For example:

```dart
// Example 2.

void main() {
void Function() f1 = () {
return 3; // Compile-time error.
};
return 3; // Compile-time error.
}
```

In this example we get a compile-time error indicating that we cannot return 3 from a function whose return type is `void`, at both occurrences of `return 3;`.

In general, this compile-time error is not required for soundness reasons, it's an opinionated rule that gives developers a heads-up when they write code that seems to imply that a given non-void value (here: `3`) is being returned to _someone_, but the value will actually (almost certainly) be ignored, because the return type is `void`. That is considered to be a likely bug, hence the error.

For a `sync*` or `async*` function it is an error to declare the return type to be `void`. This implies that when the inferred return type is `void`, a compile-time error is reported, and the developer will then know that it is a likely bug to use a `sync*` or `async*` function in that manner, just like they'd encounter a compile-time error if they were to declare a function like `void f() sync* {...}`.

For an `async` function there may or may not be a [lint message](https://dart.dev/tools/linter-rules/avoid_void_async) warning against using the return type `void` in a declaration of an `async` function. Again, we get the same kind of feedback for the regular function with return type `void` as we do for the function literal whose context type has return type `void`.

The conclusion so far is that in example 1, the function literal should have the type `void Function()` in all four cases.

However, this is not the behavior that we can observe. Here are the adjusted versions of example 1 that are accepted without compile-time errors by the analyzer respectively the CFE:

```dart
// Example 1, adjusted to fit the analyzer.

void main() {
f(() {}..expectStaticType>());
f(() sync* {}..expectStaticType Function()>>());
f(() async {}..expectStaticType Function()>>());
f(() async* {}..expectStaticType Function()>>());
}
```

```dart
// Example 1, adjusted to fit the CFE.

void main() {
f(() {}..expectStaticType>());
f(() sync* {}..expectStaticType Function()>>());
f(() async {}..expectStaticType Function()>>());
f(() async* {}..expectStaticType Function()>>());
}
```

As we can see, the tools do not use the rule mentioned above to make the inferred return type `void`, they use the kind of return type which is associated with the given kind of function body. However, they do not agree on the type argument.

It is in principle a breaking change to change the inferred return type to `void`. However, it is likely to bring out locations in code where something unintended is taking place, so it's arguably "good breakage".

So, @dart-lang/language-team, WDYT? Should we change the specification to say that the `sync*`, `async`, `async*` function literals should have a return type of the form `Iterable<...>`, `Future<...>`, respectively `Stream<...>`? In that case, what's the actual type argument? Alternatively, should we ask the tool teams to infer `void` and report a compile-time error?

Contributor guide

Open the contributing guide

Research direction

Start with the function literal return type inference section linked in the issue, then compare the adjusted analyzer and CFE examples for sync*, async, and async* literals. Done requires resolving whether the specification or the tools should change, including the inferred type argument behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.