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

Inference should flow information between arguments in a generic function call #731

Closed
leafpetersen opened this issue Jan 14, 2016 · 36 comments
Assignees

Comments

@leafpetersen
Copy link
Member

EDIT by @jmesserly

Inference should flow information up/down between arguments in a generic function call. This will help inference common methods like Iterable.fold and Future.forEach.


(original description)
We currently do downwards and upwards inference independently. This gets us a fair bit, but we should eventually integrate the two, probably following the colored local type inference approach.

@leafpetersen leafpetersen self-assigned this Jan 14, 2016
@leafpetersen leafpetersen removed their assignment Feb 24, 2016
@leafpetersen
Copy link
Member Author

@kevmoo This is the bug for integrating inference further to make things work for fold. Here's an example pulled over from dart-lang/sdk#25491 . Currently we give no error on this since we fail to infer a type for x and hence leave it as dynamic.

void main() {
  List<int> o;
  var x = o.fold(0, (x, y) => x + y);
  List<String> y = x;
}

@kevmoo
Copy link
Member

kevmoo commented Mar 11, 2016

@leafpetersen That doesn't quite hit the same thing I'm seeing

void main() {
  List<String> o;
  var x = o.fold({}, (x, y) {
    x[y] = 'thing';
    return x;
  }) as Map<String, String>;
  _printInt(x);
}

_printInt(Map<String, String> things) => print(things);

on Dart VM version: 1.16.0-dev.0.0 (Wed Mar 9 15:04:40 2016) on "macos_x64"

@leafpetersen
Copy link
Member Author

I think it does, but maybe I misunderstand. The redundant cast hint is a different issue - this is just about why it's not enough to put a type on the map literal. In your example, the cast gives inference enough context to infer the type of fold (and then you get the redundant cast warning). We ought to be able to infer the type just from the type of the receiver and the first argument - so putting a type annotation on the map literal should be enough here. It's not though - hence this bug.

Workarounds should be either typing both the map literal and the first argument to the lambda, or putting a type annotation x instead of using var.

@nex3
Copy link
Member

nex3 commented May 3, 2016

@leafpetersen asked me to add this example:

/// Creates a new map from [map] with new keys and values.
///
/// The return values of [keyFn] are used as the keys and the return values of
/// [valueFn] are used as the values for the new map.
Map/*<K2, V2>*/ mapMap/*<K1, V1, K2, V2>*/(Map/*<K1, V1>*/ map,
    /*=K2*/ keyFn(/*=K1*/ key, /*=V1*/ value),
    /*=V2*/ valueFn(/*=K1*/ key, /*=V1*/ value)) =>
  new Map.fromIterable(map.keys,
      key: (key) => keyFn(key as dynamic/*=K1*/, map[key]),
      value: (key) => valueFn(key as dynamic/*=K1*/, map[key]));

/// Creates a new map from [map] with the same keys.
///
/// The return values of [fn] are used as the values for the new map.
Map/*<K, V2>*/ mapMapValues/*<K, V1, V2>*/(Map/*<K, V1>*/ map,
    /*=V2*/ fn(/*=K*/ key, /*=V1*/ value)) =>
  // [warning] Unsound implicit cast from dynamic to K.
  mapMap(map, (key, _) => key, fn);

in mapMapValues(), I'd expect inference to be able to tell that (key, _) => key has a return type of K, but currently it cannot.

@jmesserly
Copy link

jmesserly commented Oct 5, 2017

Actually, let me repurpose this bug with a new subject line :)

We did integrate upwards and downwards inference. However, it is not able to flow information between arguments on an argument list, as required for several of the examples here such as Iterable.fold

EDIT: also to fix this, we'll need to spec this for a future version Dart and implement it in the new front end

@jmesserly jmesserly reopened this Oct 5, 2017
@jmesserly jmesserly changed the title Strong mode upwards and downwards inference need to be integrated Inference should flow information between arguments in a generic function call Oct 5, 2017
@kevmoo
Copy link
Member

kevmoo commented Oct 5, 2017

The fold case is painful. Is this really area-language?

@matanlurey
Copy link
Contributor

area-pain

@nex3
Copy link
Member

nex3 commented Oct 5, 2017

This will also become a lot more relevant if we start seriously pushing for --no-implicit-dynamic.

@jmesserly
Copy link

The fold case is painful. Is this really area-language?

Yeah it's area-language. We can't do big changes to the type system in Analyzer, because it's all migrated to the new front end (thanks to @stereotype441), and we'll need to spec out (at least informally) what the new inference algorithm should be and how the rules work. The current technique is insufficiently powerful to handle fold and similar APIs. We would need to adapt some ideas from other languages/papers (e.g. C#).

@natebosch
Copy link
Member

Has anything changed here since the last update?
Is anything likely to change here?

@jmesserly
Copy link

jmesserly commented Jun 12, 2018

Has anything changed here since the last update?
Is anything likely to change here?

This inference improvement (if it happens, which I hope it does) is post 2.0, so there won't be any changes until after that.

edit: tagged this as "feature request"

@leafpetersen
Copy link
Member Author

Yeah, let's keep this open for now until we re-organize language requests. This is something I'd still like to try to do in a future inference revision.

@alexander-doroshko
Copy link

Also reported here: https://youtrack.jetbrains.com/issue/WEB-34819

@eernstg
Copy link
Member

eernstg commented Apr 1, 2019

One more example where an issue in this area was reported, again using fold: dart-lang/sdk#36405.

@mnordine
Copy link
Contributor

mnordine commented Apr 21, 2019

Just want to chime in here that I've ran into this unexpected behavior. I've been migrating a large code base to Dart 2 with implicit-casts: false, implicit-dynamic: false, and having to go in and manually adding the generic type hints in hundreds of spots is annoying indeed. Would love to see this fixed! I like using fold and this feels like paying a little tax on it each time.

@eernstg
Copy link
Member

eernstg commented Jun 13, 2019

Here is another example: dart-lang/sdk#37247.

@leafpetersen leafpetersen transferred this issue from dart-lang/sdk Dec 8, 2019
dart-bot pushed a commit to dart-lang/sdk that referenced this issue Jun 23, 2020
Dart type inference does not propagate type sideways in the argument lists[1],
so we have to explicitly provide fold with a type argument, otherwise
it is inferred as `Object?`, which does not work in this case.


[1] dart-lang/language#731

Change-Id: I2ee0bda2cd6c37c64018efd48ea14c469f76ba3a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/152003
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Vyacheslav Egorov <vegorov@google.com>
@mnordine
Copy link
Contributor

mnordine commented Oct 2, 2020

@leafpetersen Is this more of an issue now with nnbd?

I'm not sure if this is applicable to this particular issue, but somebody posted the following example in Gitter, complaining of the lack of inference. I tried with nnbd and it results in a static error:

Iterable<R> map<T, R>(
  Iterable<T> iterable,
  R Function(T) transform,
) sync* {
  for (var item in iterable) {
    yield transform(item);
  }
}

void main() {
  var list = [1, 2, 3];
  var mappedList = map(list, (x) => x + 1); // `x` is inferred as `Object?`
  print(mappedList);
}

An expression whose value can be 'null' must be null-checked before it can be dereferenced - line 12

@leafpetersen
Copy link
Member Author

@leafpetersen Is this more of an issue now with nnbd?

Yes, or no, depending how you look at it. nnbd will make this more statically visible, which has both upsides and downsides.

Your example used to work because x was typed as dynamic. This is nice, because the code in fact runs correctly. But it's also not nice because code that doesn't run correctly will also pass the static analysis.

void main() {
  var list = [1, 2, 3];
  var mappedList = map(list, (x) => x.foo); // `x` is inferred as `Object?`
  print(mappedList);
}

This variant of your main function will pass static analysis in pre-nnbd Dart, but blow up at runtime.

But I do expect that nnbd will raise the profile of this issue, yes.

@kasperpeulen
Copy link

kasperpeulen commented Oct 2, 2020

I actually love that nnbd raises an error.
The pre nnbd variant way doesnt look sound at all, as it silently infers it to dynamic it seems.

But you will get more people complaining 😅. If something blows up at runtime, the programmer get blamed by their manager. When something doesnt compile that theoretically should, the language designers gets blamed.

@mnordine
Copy link
Contributor

mnordine commented Oct 2, 2020

Sure, and I'd love inferring the anonymous function parameter even more, :)

copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 19, 2022
Previously, the front end would perform downwards and upwards
inference using separate TypeConstraintGatherer objects.  This meant
that any constraint gathering work performed during downwards
inference had to be repeated during upwards inference.

This change avoids the extra work by using a single
TypeConstraintGatherer object for both downwards and upwards
inference.

This should help pave the way to implementing support for
dart-lang/language#731 (improved inference
for fold etc.)

Change-Id: Ib4031ab1397d6a8547a705f386632de0e4dd1a2b
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241120
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 20, 2022
…"named arguments anywhere".

Hoisting function literals isn't necessary because evaluating a
function literal doesn't have any side effects.

This paves the way for adding support for
dart-lang/language#731 (improved inference
for fold etc.), which will require deferring type analysis of function
literals.  By not hoisting them, we will be able to avoid a lot of
unnecessary complexity in the logic to defer them.

Change-Id: I0a49aa9f769f0506569e6029a3697898308600c2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241007
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 21, 2022
In order to support dart-lang/language#731
(improved inference for fold etc.) I'm going to need to add logic to
_inferInvocation to postpone type analysis of arguments that are
function expressions.  To avoid having to code up this logic twice, it
will be helpful to have both named and unnamed arguments handled by
the same chunk of code.

In particular, this change unifies the computation of
inferredFormalType, the recursive call to inferExpression, the logic
for hoisting, and the update of the local variables identicalInfo,
formalTypes, and actualTypes.  We pay a small price by having to have
multiple `if (isExpression)` checks, but these should be very fast.

Change-Id: I095a7eac84237eeb878cc3dd86e76a6a871f31d5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241041
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 21, 2022
In order to implement dart-lang/language#731
(improved inference for fold etc.), we'll need to gradually accumulate
type constraints as we evaluate arguments, and periodically re-do
inference; we can't just accumulate all the formal types and actual
types and run them through an inference process at the end.

This change moves us a step toward that eventuality, by accumulating
type constraints after type inference visits each argument, rather
than all at once after all arguments have been visited.  The total
amount of work done is unchanged.

Change-Id: I91ed0529cd3142afe4153cac8c25bce3c20f137d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241800
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 27, 2022
These tests exercise the "deferred type inference of function
literals" part of dart-lang/language#731
(improved inference for fold etc.) for super-constructor invocations
and redirecting constructor invocations, both of which have their own
code paths in the analyzer.

Change-Id: I6877ac3c07a3cca31550ba74d941d250c8410cfd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241987
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 28, 2022
…led.

In order to address dart-lang/language#731
(improved type inference for `fold` etc.) we're going to need to
sometimes defer analysis of invocation arguments that are closures, so
that closure parameters can have their types inferred based on other
parameters.  To avoid annoying the user with inconsistent behaviors,
we defer analysis of closures in all circumstances, even if it's not
necessary to do so for type inference purposes.

This has a minor user-visible effect: if an invocation contains some
closures and some non-closures, any demotions that happen due to write
captures in the closures are postponed until the end of the
invocation; this means that the write-captured variables remain
promoted for other invocation arguments, even if those arguments
appear after the closure.  This is safe because there is no way for
the closure to be called until after all of the other invocation
arguments are evaluated.  See the language tests in
tests/language/inference_update_1/write_capture_deferral_enabled_test.dart
for details.

Note that this change only has an effect when the experimental feature
`inference-update-1` is enabled.

Change-Id: If7bb38e361755180c033ecb2108fc4fffa7570b1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/241864
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 28, 2022
In the definition of `_computeExplicitlyTypedParameterSet`, I
accidentally nested the declaration of `unnamedParameterIndex` inside
the `for` loop, defeating the increment and causing all parameters to
be considered to have index 0.

I've included a test case that would have caught the mistake.

Bug: dart-lang/language#731
Change-Id: I0cd0e1e5b481313150e495d370af2477253d6637
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/242741
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
@leafpetersen leafpetersen moved this from Being spec'ed to Being implemented in Language funnel Apr 28, 2022
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue Apr 29, 2022
When a function literal is wrapped in parentheses, it shouldn't affect
how it interacts with type inference.  This change ensures that
parenthesized function literals are treated the same as
unparenthesized ones by the logic that supports
dart-lang/language#731 (improved inference
for fold etc.)

Change-Id: I672787a31addbfe3f3282b6e638e00b693eea46f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243000
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue May 3, 2022
This change allows function literals in invocations to be inferred in
dependency order.  For example, given the following code:

    U f<T, U>(T Function() g, U Function(T) h) => h(g());
    test() {
      var x = f(() => 0, (y) => [y]);
    }

The function literal `() => 0` is inferred first, causing the type
parameter `T` to be assigned the type `int`.  Then, `(y) => [x]` is
inferred with the benefit of this type assignment, so `y` gets the
type `int`, and consequently, `U` gets assigned the type `List<int>`.

This completes the support for
dart-lang/language#731 (improved inference
for fold etc.)

Change-Id: I48c22693720a1cc8bbf435643e863834e07f02b1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243002
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
stereotype441 added a commit to dart-lang/co19 that referenced this issue May 6, 2022
The function passed to Stream.handleError needs to accept either a
single argument of type `Object`, or an `Object` and a `StackTrace`.
Since this can't be expressed by the type system, it's checked at
runtime.  As a result, the `check` function (as previously written)
would fail if ever instantiated with a type `T` that was narrower than
`Object`.

Prior to implementation of
dart-lang/language#731 (improved inference
for fold etc.), this worked fine, because type inference always
supplied an argument type of `dynamic` or `Object` for the function
literals passed to `check`.  However, with the inference improvement
enabled, it starts to infer other types, causing runtime failures.

The correct fix is to give the `event` argument of `onError` an
appropriate type.
stereotype441 added a commit to dart-lang/co19 that referenced this issue May 9, 2022
The function passed to Stream.handleError needs to accept either a
single argument of type `Object`, or an `Object` and a `StackTrace`.
Since this can't be expressed by the type system, it's checked at
runtime.  As a result, the `check` function (as previously written)
would fail if ever instantiated with a type `T` that was narrower than
`Object`.

Prior to implementation of
dart-lang/language#731 (improved inference
for fold etc.), this worked fine, because type inference always
supplied an argument type of `dynamic` or `Object` for the function
literals passed to `check`.  However, with the inference improvement
enabled, it starts to infer other types, causing runtime failures.

The correct fix is to give the `event` argument of `onError` an
appropriate type.
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue May 10, 2022
The front end desugars extension methods by inserting a synthetic
`this` argument before all other arguments.  But this synthetic
argument isn't included in the `formalTypes` and `actualTypes` arrays.
So when recording a value into
`_DeferredParamInfo.evaluationOrderIndex` we may need to subtract 1 in
order to ensure that later logic will find the correct argument.

Fixes a corner case of dart-lang/language#731.

Change-Id: Idbf136195e40555199f7c5b61a575a430f6ec6bd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243854
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue May 10, 2022
… inference stages.

As part of the implementation of
dart-lang/language#731 (improved inference
for fold etc.), I expanded the front end's type inference logic so
that instead of just having a downward phase and an upward phase, it
could have 3 or more phases.  The function that previously did
downward inference became repurposed to do "partial inference" (which
could either be the first, downward stage, or a later, horizontal
stage).  However, I failed to generalize the logic that prevents types
assigned by one inference stage from being refined by later
stages--previously this logic was only needed for upward inference,
but now it's needed for horizontal inference stages as well.  (This
logic is needed because of Dart's "runtime checked covariance"
behavior--it means that we want to stick with the type from downward
inference, even if a later horizontal inference stage is able to find
a more precise type, because that more precise type may lead to
runtime failures).

As part of this change I've re-architected the inference methods so
that they are responsible for creating and returning the list of
inferred types.  This makes the inference logic more similar between
the front end and analyzer, and is easier to read IMHO.  The total
number of list allocations is the same as before.

Change-Id: I19bfcede9c2968e50f110b571164549f16495217
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243707
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
copybara-service bot pushed a commit to dart-lang/sdk that referenced this issue May 24, 2022
Fixes dart-lang/language#731.

Change-Id: I5fee1470efe7b891b79dcfecd33bc3670590efb3
Tested: trybots, and global presubmit in the internal mono repo
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243530
Reviewed-by: Bob Nystrom <rnystrom@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Liam Appelbe <liama@google.com>
Reviewed-by: Michael Thomsen <mit@google.com>
@stereotype441
Copy link
Member

This is fully implemented and enabled as of 0a92b0c3aa2e32924a04c7a12a544f98b611fea4. It was released to the dev channel in 2.18.0-150.0.dev.

@lulupointu
Copy link

lulupointu commented Jun 23, 2022

@stereotype441 Thanks for this amazing update !

However I don't know if this is expected but the following does not work:

List<num> numList;
...
List<int> intList = [1, 2, 3];
numList = [
  intList.fold(0, (x, y) => x.bitLength + y), // Error: The getter 'bitLength' isn't defined for the type 'num'.
];

While the following does work:

List<int> l1 = [1, 2, 3];
List<double> l2 = [1.0, 2.0, 3.0];
final a = [
  l1.fold(0, (x, y) => x.bitLength + y),
  l2.fold(0.0, (x, y) => x + y),
];
print(a is List<num>); // true

@pedromassango
Copy link

@felangel can this prevent the need to specify the state when declaring a BlocBuilder widget?

From (current): BlocBuilder<MyCubit, MyState>(...)
To (new/wanted): BlocBuilder<MyCubit>(...)

@felangel
Copy link

felangel commented Oct 29, 2022

@felangel can this prevent the need to specify the state when declaring a BlocBuilder widget?

From (current): BlocBuilder<MyCubit, MyState>(...) To (new/wanted): BlocBuilder<MyCubit>(...)

Unfortunately I don't think it does see #620

@eernstg
Copy link
Member

eernstg commented Oct 31, 2022

I think it would be useful to clarify the situation around this comment. Here is a simpler example showing the same phenomenon as the example given there:

X f<X>(X x, X Function(X) g) => g(x);

void main() {
  var i = 1;
  num n = f(i, (j) => j.bitLength); // 'The getter `bitLength` isn't defined for the type `num`'.
}

In this case (and in the original example) there is no direct connection to the support for horizontal inference (which is the new feature that was introduced to resolve this issue). The reason why the inferred actual type argument to the invocation of f is num rather than int is that the context type is num. That is, this inference result is caused by declaring n as num n, rather than declaring it, e.g., as var n or int n.

This means that there is no reasonable way we could change horizontal inference such that these examples would infer int rather than num. So there is no reason to have that discussion here.

It is a separate discussion that Dart type inference gives priority to the context type, rather than choosing a type which is obtained by plain bottom-up inference. Check out the issues about inference in general, which currently shows at least four issues about how to handle context types. The standard example showing that the context type cannot be ignored in Dart could be this one:

// Assume that Dart would give priority to bottom-up inference,
// rather than giving priority to the context type.

void main() {
  List<num> nums = []; // Inferred as `<Never>[]`.
  nums.add(1); // Throws.
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

No branches or pull requests