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

fix(compiler): evaluate safe navigation expressions in correct bindin… #37911

Closed
wants to merge 1 commit into from

Conversation

JoostK
Copy link
Member

@JoostK JoostK commented Jul 3, 2020

…g order

When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:

<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>

The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:

var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);

Notice that the pipe evaluation happens before evaluating the enabled binding,
such that the runtime's internal binding index would correspond with enabled,
not firstName. This introduces a problem when the pipe uses WrappedValue to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with enabled as dirty, instead of firstName. This results
in the enabled binding to be updated, triggering setters and affecting how
OnChanges is called.

In the pseudo-code above, the intermediate firstName variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:

var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);

This commit introduces a new BindingForm that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.

Fixes #37194

@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 3, 2020
@JoostK JoostK force-pushed the ivy-pipe-binding-ooo branch from 8c40469 to 5a25efe Compare Jul 3, 2020
@JoostK JoostK marked this pull request as ready for review Jul 3, 2020
@pullapprove pullapprove bot requested a review from alxhub Jul 3, 2020
Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

LGTM, overall, but as discussed offline I would like to see one more test where a component uses ngOnChanges as reported in #37194. Also the CI size check is failing so it will need cleanup (hence the appropriate label).

Thnx so much for this PR, must have been fun to track down :-) Oh, and the PR description / commit message is absolutely excellent 👍

Reviewed-for: fw-core

…g order

When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:

```html
<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>
```

The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:

```js
var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);
```

Notice that the pipe evaluation happens before evaluating the `enabled` binding,
such that the runtime's internal binding index would correspond with `enabled`,
not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with `enabled` as dirty, instead of `firstName`. This results
in the `enabled` binding to be updated, triggering setters and affecting how
`OnChanges` is called.

In the pseudo-code above, the intermediate `firstName` variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:

```js
var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);
```

This commit introduces a new `BindingForm` that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.

Fixes angular#37194
@JoostK JoostK force-pushed the ivy-pipe-binding-ooo branch from ad36b48 to 855149f Compare Aug 3, 2020
@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 3, 2020

Copy link
Member

@pkozlowski-opensource pkozlowski-opensource left a comment

LGTM, thnx for adding all the runtime tests!

// https://github.com/angular/angular/issues/37194
// https://github.com/angular/angular/issues/37591
// Using a pipe in the LHS of safe navigation operators would clobber unrelated bindings
// iff the pipe returns WrappedValue, incorrectly causing the unrelated binding

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"iff" typo?

Copy link
Member Author

@JoostK JoostK Aug 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a deliberate "if-and-only-if"

@JoostK JoostK removed the request for review from alxhub Aug 11, 2020
AndrewKushnir added a commit that referenced this issue Aug 11, 2020
…g order (#37911)

When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:

```html
<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>
```

The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:

```js
var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);
```

Notice that the pipe evaluation happens before evaluating the `enabled` binding,
such that the runtime's internal binding index would correspond with `enabled`,
not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with `enabled` as dirty, instead of `firstName`. This results
in the `enabled` binding to be updated, triggering setters and affecting how
`OnChanges` is called.

In the pseudo-code above, the intermediate `firstName` variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:

```js
var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);
```

This commit introduces a new `BindingForm` that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.

Fixes #37194

PR Close #37911
profanis added a commit to profanis/angular that referenced this issue Sep 5, 2020
…g order (angular#37911)

When using the safe navigation operator in a binding expression, a temporary
variable may be used for storing the result of a side-effectful call.
For example, the following template uses a pipe and a safe property access:

```html
<app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view>
```

The result of the pipe evaluation is stored in a temporary to be able to check
whether it is present. The temporary variable needs to be declared in a separate
statement and this would also cause the full expression itself to be pulled out
into a separate statement. This would compile into the following
pseudo-code instructions:

```js
var temp = null;
var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name;
property('enabled', ctx.enabled)('firstName', firstName);
```

Notice that the pipe evaluation happens before evaluating the `enabled` binding,
such that the runtime's internal binding index would correspond with `enabled`,
not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to
force a change to be detected, as the runtime would then mark the binding slot
corresponding with `enabled` as dirty, instead of `firstName`. This results
in the `enabled` binding to be updated, triggering setters and affecting how
`OnChanges` is called.

In the pseudo-code above, the intermediate `firstName` variable is not strictly
necessary---it only improved readability a bit---and emitting it inline with
the binding itself avoids the out-of-order execution of the pipe:

```js
var temp = null;
property('enabled', ctx.enabled)
  ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name);
```

This commit introduces a new `BindingForm` that results in the above code to be
generated and adds compiler and acceptance tests to verify the proper behavior.

Fixes angular#37194

PR Close angular#37911
@angular-automatic-lock-bot
Copy link

@angular-automatic-lock-bot angular-automatic-lock-bot bot commented Sep 11, 2020

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.