Skip to content

The flatten function depends on static type, so it should be declared for type variables. #2326

Closed
@lrhn

Description

@lrhn

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 variable X with bound B, then flatten(T) = flatten(B)
  • If T is promoted type variable X & 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThere is a mistake in the language specification or in an active document

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions