dart-lang / dart-lang/language
Inconsistent inferred return type of function literal with specialized body
- 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
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