How should we expect this code to be analyzed?
dynamic f() sync* {
yield* g()..foo();
}
T g<T>() => throw 'T=$T';
main() {
for (var x in f()) {
print('x=$x');
}
}
Currently, the analyzer and CFE both accepts this code without issuing any compile-time errors. The runtime behavior is to print:
Unhandled exception:
T=dynamic
#0 g (file:///home/paulberry/tmp/proj/test.dart:5:13)
#1 f (file:///home/paulberry/tmp/proj/test.dart:2:10)
#2 _SyncStarIterator.moveNext (dart:async-patch/async_patch.dart:595:13)
#3 main (file:///home/paulberry/tmp/proj/test.dart:8:17)
#4 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#5 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
This indicates that the context type being passed to the operand of yield* is either the unknown type (_) or dynamic. Reading through the implementation, I'm pretty certain it's the unknown type.
But resources/type-system/inference.md says:
- If the enclosing function is marked
sync*, then for each yield* e; statement in the block, let S be the inferred type of e, using the local type inference algorithm described below with a typing context of Iterable<K>; let E be the type such that Iterable<E> is a super-interface of S; and update T to be UP(E, T).
Which suggests that the context should be Iterable<K> for some K.
K is defined to be "the typing context for the function body as computed above from the imposed return type schema." It's not entirely clear what this refers to, but the definition of the "imposed return type schema" is as follows:
The return type of the context function type is used at several points during inference. We refer to this type as the imposed return type schema. Inference for each returned or yielded expression in the body of the function literal is done using a context type derived from the imposed return type schema S as follows:
- If the function expression is neither
async nor a generator, then the context type is S.
- If the function expression is declared
async* and S is of the form Stream<S1> for some S1, then the context type is S1.
- If the function expression is declared
sync* and S is of the form Iterable<S1> for some S1, then the context type is S1.
- Otherwise, without null safety, the context type is
FutureOr<flatten(T)> where T is the imposed return type schema; with null safety, the context type is FutureOr<futureValueTypeSchema(S)>.
It's difficult to say whether this spec text is meant to apply to the function f in the code example above. On the one hand, the spec text is clearly talking about a "function expression" (which the function f is not). On the other hand, there are no other mentions of yield* anywhere else in the doc specifying the expected behavior when the yield* appears in a function rather than a function expression.
If we assume the text is intended to apply in this case, then it a literal reading of it produces suggests that the imposed return type schema should be FutureOr<futureValueTypeSchema(_)>, which is FutureOr<_>. But this can't be what we want, because it never makes sense for the operand of yield* to be a future.
To make matters worse, the current behavior (where a return type of dynamic causes the operand of yield* to be inferred with a type context of _) is not covered by any tests. (I found this out by breaking it and seeing that no tests failed).
I'm not 100% sure what the right fix is here; I'm still researching. Filing this issue to document what I've learned so far.
How should we expect this code to be analyzed?
Currently, the analyzer and CFE both accepts this code without issuing any compile-time errors. The runtime behavior is to print:
This indicates that the context type being passed to the operand of
yield*is either the unknown type (_) ordynamic. Reading through the implementation, I'm pretty certain it's the unknown type.But resources/type-system/inference.md says:
Which suggests that the context should be
Iterable<K>for someK.Kis defined to be "the typing context for the function body as computed above from the imposed return type schema." It's not entirely clear what this refers to, but the definition of the "imposed return type schema" is as follows:It's difficult to say whether this spec text is meant to apply to the function
fin the code example above. On the one hand, the spec text is clearly talking about a "function expression" (which the functionfis not). On the other hand, there are no other mentions ofyield*anywhere else in the doc specifying the expected behavior when theyield*appears in a function rather than a function expression.If we assume the text is intended to apply in this case, then it a literal reading of it produces suggests that the imposed return type schema should be
FutureOr<futureValueTypeSchema(_)>, which isFutureOr<_>. But this can't be what we want, because it never makes sense for the operand ofyield*to be a future.To make matters worse, the current behavior (where a return type of
dynamiccauses the operand ofyield*to be inferred with a type context of_) is not covered by any tests. (I found this out by breaking it and seeing that no tests failed).I'm not 100% sure what the right fix is here; I'm still researching. Filing this issue to document what I've learned so far.