Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

type inference in for-each loop is inconsistent with behavior elsewhere #54706

Closed
trinarytree opened this issue Jan 23, 2024 · 4 comments
Closed
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-as-intended Closed as the reported issue is expected behavior

Comments

@trinarytree
Copy link

the following code

void main() {
  final Iterable<int>? xs = null;
  final ys = xs ?? [];
  print(ys.runtimeType);        // List<int>
  final List<int>? zs = null;

  for (final x in xs ?? []) {}  // compiler error
  for (final x in ys) {}        // no problem
  for (final x in zs ?? []) {}  // no problem
}

yields a compiler error on the xs ?? [] loop, but not the ys loop.
this is counterintuitive and annoying. one would think that whatever type the compiler inferred ys is
it should also infer xs ?? [] is in its for loop since it's the same expression.
notice that the zs ?? [] loop does not have a compiler error.
the difference here is that zs is a List, while xs is an Iterable.
i feel like this shouldn't matter, but somehow it does.

why should putting xs ?? [] into a variable and then using that variable
yield different behavior than simply inlining it?

a workaround is to put xs ?? <int>[] into the loop, but we only need this seemingly
superfluous type annotation when xs is an Iterable rather than a List,
and for some reason, we don't need it when declaring ys.

is this a bug relative to spec, or some unintended consequence of the spec?

dart --version
Dart SDK version: 3.2.4 (stable) (Thu Dec 21 19:13:53 2023 +0000) on "macos_x64"

@lrhn
Copy link
Member

lrhn commented Jan 23, 2024

This is not unique to for loops. It happens when the ?? expression has a context type, and that context type gives the [] an element type which is not the same as the element type of xs.

  Iterable<num> i4 = xs ?? []; // compile-time error.

This issue requires the expression to have a context type, which is applied as context type for the second operand. Otherwise it would use the type of the first operand, made non-nullable, as the context type of [], and then it would choose the element type of xs, which works.)
In the error case, the context type for [] becomes Iterable<num>, which means that the type of [] becomes List<num>.

And then the least upper bound of Iterable<int> and List<num?> is Object. That's a known shortcoming of the least-upper-bound algorithm.

It's operating the way it is for a reason, which mostly, or at least often, works, and it could have worked (and given xs ?? [] with context Iterable<num> a type of Iterable<num>) if it wasn't for the least-upper-bound algorithm being particularly bad at handling cases like List<X>/Iterable<Y> where Y is not a subtype of X.

@a-siva a-siva added the area-front-end Use area-front-end for front end / CFE / kernel format related issues. label Jan 23, 2024
@trinarytree
Copy link
Author

In the error case, the context type for [] becomes Iterable<num>, which means that the type of [] becomes List<num>

i don't understand. why would the context for assignment to ys be any different from the context in the xs loop? also, where does this num come from? i mean, yeah, it's an ancestor of int, but why is any of this code interested in a proper ancestor of int other than perhaps Object?

@lrhn lrhn added area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). and removed area-front-end Use area-front-end for front end / CFE / kernel format related issues. labels Jan 24, 2024
@lrhn
Copy link
Member

lrhn commented Jan 24, 2024

The num comes from my Iterable<num> i4 = ... example.

Dart performs type inference with an optional context type, if it can see that the context expects a specific type, or an empty context if it can't see any requirements.

The inference of e1 ?? e2 behaves differently depending on whether it has a type context.
If it has context type C, the context type for e1 is C? and the context type for e2 is C.
If it does not have a context type, then e1 has no context type either, but if the static type found for e1 is T, then the context type for e2 becomes NonNull(T).
That's precisely because List<int>? p = ...; var x = p ?? []; usually intends [] to be a <int>[], and we have no better hint.

The assignment to ys has no context type, because var ys = does not imply any requirements.
Because of that var ys = xs ?? []; infers x to have type Iterable<int>?, then infers [] with context type Iterable<int>, and makes it <int>[]. Then it successfully finds the upper bound of Iterable<int> and List<int>, and all is well.

For for (var x in xs ?? []) ..., the xs ?? [] has a context type of Iterable<_>. We know it must be an iterable, because that's a type requirement on the expression.
That means the context type of xs is Iterable<_>? and for [] it's Iterable<_>, which leads to it being inferred as <Object?>[].

So, [] ends up with different types in the two cases, because one has a context type and the other does not.
And that's fine.

Then we hit the problem that least-upper-bound of Iterable<int> and List<Object?> is Object. That's highly annoying, and a separate problem that we're well aware of. The hope is to find a way to, at least, make sure that the least-upper-bound of two types, in a context with a type requirement that both types satisfy, should at worst choose the context type, and not just Object like today.
But that hasn't happened yet.

@lrhn lrhn added the closed-as-intended Closed as the reported issue is expected behavior label Jan 24, 2024
@FMorschel
Copy link
Contributor

FMorschel commented Jan 24, 2024

One other way of solving the compile-time error would be:

for (final x in [...?xs]) {} // no problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-language Dart language related items (some items might be better tracked at github.com/dart-lang/language). closed-as-intended Closed as the reported issue is expected behavior
Projects
None yet
Development

No branches or pull requests

4 participants