Skip to content

Adjust type inference of ?? in the empty context #4516

@eernstg

Description

@eernstg

Thanks to @osa1 for bringing up this topic!

The expression e1 ?? e2 infers the type of e2 in the context type schema of the whole expression, if there is one. However, when there is no context type schema for the entire expression (that is, it's _), the static type of e1 is used as the context type schema for e2.

This implies that we get a more imprecise inference result when the type of e1 isn't greater or equal than the type of e2. For example, e1 could be a List<...> and e2 could be an iterable:

Iterable<X> getIterable<X>() => <X>[];

void main() {
  final List<int>? xs = null;
  var ys = xs ?? getIterable();
  print(ys.runtimeType); // `List<dynamic>`.
}

We could change the computation of the context type schema for e2 when e1 ?? e2 occurs in the empty context _:

Let S be the static type of e1 (inferred in the empty context _). Let S1 be the union-free erasure of S (so if S is List<T>? then S1 is List<T>, and similarly for FutureOr). If S1 is not an interface type then the context type for e2 is NonNull(S). Otherwise, let S2 be the unique type in the superinterface graph of S1 which has the greatest depth, and is also a supertype of the most special generic instantiation of the type of e2 after union-free erasure. If there is no such type then e2 again gets context type NonNull(S). Otherwise, let S3 be S2 with the union types of S added back in. Then NonNull(S3) is used as the context type for the inference of e2.

Edit: Computing S2 may incur approximately the same amount of work as performing type inference, so we might want to do something which is simpler, and a conservative approximation.

Note that S is actually a type, not a type schema, because it is the result of inference on e1. Also note that "unique, with the greatest depth" is the same criterion as the last item in the computation of UP, that is, the Dart 1 algorithm for the computation of the "least" upper bound. However, the treatment of type arguments in the type of e2 is different: They are essentially ignored. So we're looking for the most special (hence: most informative) supertype of the type of e1 for which we can hope to make the static type of e2 a subtype using inference.

In the example above, S is List<int>?, S1 is List<int>, S2 is Iterable<int>, and S3 is Iterable<int>?, which means that Iterable<int> is used as the context type for the inference of getIterable(), yielding getIterable<int>().

See #4518 for a situation where this really matters.

@dart-lang/language-team, WDYT?

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problemstype-inferenceType inference, issues or improvements

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions