Description
The flatten function determines/reflects the behavior of await
on an expression based on the expression's static type.
The function is not completely defined since it relies on static type, but isn't defined for a type variable.
Example:
Future<void> foo<T>(T value) async {
var x = await value;
print([x].runtimeType); // JSArray<Object>, so x is Object
print(x); // null
}
By the specification, the static type of value
doesn't match any of the flatten cases (it's a type variable), so it should hit the last case and make the static type T
. Then at runtime it should check is Future<T>
before awaiting.
We currently have an unsoundness:
Future<void> foo<T>(T value) async {
var x = await value;
print([x].runtimeType); // JSArray<Object>, so x is Object
print(x); // null
}
void main() async {
await foo<Object>(Future<Object?>.value(null));
}
It's not clear whether this is just another instance of #2310 - the internal check of is Future<T>
is skipped before awaiting.
However, if we do:
Future<void> bar<T extends Future<Object>>(T value) {
var x = await value;
print([x].runtimeType);
}
we'd probably expect that await
to always await the value. The flatten, as written, still reaches its last case and makes the static type of x
be T
.
Our implementations do better:
Future<void> bar<T extends Future<A>>(T value) async {
var x = await value;
print([x].runtimeType); // JSArray<A>
if (value is Future<B>) {
var y = await value; // value has static type `T & Future<B>`
print([y].runtimeType); // JSArray<B>
}
}
void main() async {
bar<Future<A>>(Future<B>.value(B()));
}
class A {}
class B extends A {}
So, we actually do use the bound/promotion of a type variable (at least in some situations).
I propose making this all explicit by adding the following cases to flatten:
- If
T
is type variableX
with boundB
, then flatten(T
) = flatten(B
)- If
T
is promoted type variableX & S
, then flatten(T
) = flatten(S
)- Otherwise ...
The runtime behavior would be that where we do "If the static type is ..., do ..." we say that "If the static type is ... or it is X with bound B and B is ... or it is X&S and S is ..., do ...". That is, in every way, treat a type variable the same as its bound/promotion around await
.
This makes the behavior explicit, and removes the type variable itself from the result.
We should then add tests to ensure we have consistent behavior (I only tested in DartPad).