-
Notifications
You must be signed in to change notification settings - Fork 46.9k
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
[compiler] Patch ValidatePreserveMemo to bailout correctly for refs #30603
Conversation
[ghstack-poisoned]
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
ghstack-source-id: 3f5d5c15bae9f6523a572fff879274559a490c38 Pull Request resolved: #30603
… for refs" --- **Background** ValidatePreserveExistingMemo checks that useMemos are preserved by checking that the returned result of the existing `useMemo`/`useCallbacks` either: 1. is never assigned a reactive scope (e.g. inferred to be a global or primitive) 2. has an *unpruned* reactive scope We prune scopes for a few reasons. This is a bit overly conservative as there are valid reasons for pruning (see special-casing for `PruneAlwaysInvalidatingScopes`). Note that this bugfix actually exposes some of these. We can either special-case more pruning passes to be valid (potentially introducing `PrunedScope.reason`) or continue to be conservative to retain source semantics. **This PR** This PR fixes a bug around ValidatePreserveUseMemo + iife inlining. For useMemos that are successfully inlined, we previously always hit case 1: assuming that useMemo return values don't need reactive scopes due to type inference. When inlining useMemo, we convert the useMemo to an iife and inline it, synthesizing a temporary variable for recording the iife return value. ```js // source useMemo(() => foo(), ...); // hir $0 = Decl t$0 $1 = LoadGlobal "foo" $2 = CallExpression $1 $3 = Reassign t$0 = $2 $4 = FinishMemoize decl=t$0 // validation marker ``` `$2` gets assigned a reactive scope. But `t$0` is never mutated after the reassignment (note that this is impossible by construction -- `validatePreserveExistingMemo` would fail earlier in inferReferenceEffects in that case), so it does not get a scope. This PR patches `validatePreserveExistingMemo` to check through reassignments to the `FinishMemoize` value. Since FinishMemoize only ever records rvalues (the result of the useMemo / useCallback call itself), this should only affect inlined useMemos. Following the same example, we previously would check that `t$0` preserves existing memo semantics. Now, we check `$2` instead. TODO: test by syncing internally [ghstack-poisoned]
ghstack-source-id: 7013d50a4eae1fdbf85a3fd996f4daa4235374c0 Pull Request resolved: #30603
useHook(); | ||
x.push(props); | ||
|
||
return useMemo(() => [x], [x]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note this bails out because we don't have the same special-casing for pruning always-invalidating scopes as we do for non-escaping scope decls.
Hopefully this is a rare case, although it makes sense to add special handling for it (more so than the logic we already have for PruneNonEscapingScopes, which arguably changes program semantics)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense to add special-casing for pruning of always invalidating scopes to avoid this false positive
… for refs" --- **Background** ValidatePreserveExistingMemo checks that useMemos are preserved by checking that the returned result of the existing `useMemo`/`useCallbacks` either: 1. is never assigned a reactive scope (e.g. inferred to be a global or primitive) 2. has an *unpruned* reactive scope We prune scopes for a few reasons. This is a bit overly conservative as there are valid reasons for pruning (see special-casing for `PruneAlwaysInvalidatingScopes`). Note that this bugfix actually exposes some of these. We can either special-case more pruning passes to be valid (potentially introducing `PrunedScope.reason`) or continue to be conservative to retain source semantics. **This PR** This PR fixes a bug around ValidatePreserveUseMemo + iife inlining. For useMemos that are successfully inlined, we previously always hit case 1: assuming that useMemo return values don't need reactive scopes due to type inference. When inlining useMemo, we convert the useMemo to an iife and inline it, synthesizing a temporary variable for recording the iife return value. ```js // source useMemo(() => foo(), ...); // hir $0 = Decl t$0 $1 = LoadGlobal "foo" $2 = CallExpression $1 $3 = Reassign t$0 = $2 $4 = FinishMemoize decl=t$0 // validation marker ``` `$2` gets assigned a reactive scope. But `t$0` is never mutated after the reassignment (note that this is impossible by construction -- `validatePreserveExistingMemo` would fail earlier in inferReferenceEffects in that case), so it does not get a scope. This PR patches `validatePreserveExistingMemo` to check through reassignments to the `FinishMemoize` value. Since FinishMemoize only ever records rvalues (the result of the useMemo / useCallback call itself), this should only affect inlined useMemos. Following the same example, we previously would check that `t$0` preserves existing memo semantics. Now, we check `$2` instead. TODO: test by syncing internally [ghstack-poisoned]
ghstack-source-id: 73fc47f23488b392644ffa27f124c309dc82b69c Pull Request resolved: #30603
/** | ||
* Track reassignments so we can correctly set `pruned` flags for | ||
* inlined useMemos. | ||
*/ | ||
reassignments: Map<DeclarationId, Set<Identifier>> = new Map(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same logic (and comments) as in ValidatePreserveManualMemo. This was needed to avoid a regression of 0c86667
[...decls].every( | ||
decl => decl.scope == null || this.prunedScopes.has(decl.scope.id), | ||
) | ||
) { | ||
instruction.value.pruned = true; | ||
value.pruned = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See newly added test fixtures for why this is needed
… for refs" --- **Background** ValidatePreserveExistingMemo checks that useMemos are preserved by checking that the returned result of the existing `useMemo`/`useCallbacks` either: 1. is never assigned a reactive scope (e.g. inferred to be a global or primitive) 2. has an *unpruned* reactive scope We prune scopes for a few reasons. This is a bit overly conservative as there are valid reasons for pruning (see special-casing for `PruneAlwaysInvalidatingScopes`). Note that this bugfix actually exposes some of these. We can either special-case more pruning passes to be valid (potentially introducing `PrunedScope.reason`) or continue to be conservative to retain source semantics. **The bug** When inlining useMemo, we convert the useMemo to an iife and inline it, synthesizing a temporary variable for recording the iife return value. ```js // source useMemo(() => foo(), ...); // hir $0 = Decl t$0 $1 = LoadGlobal "foo" $2 = CallExpression $1 $3 = Reassign t$0 = $2 $4 = FinishMemoize decl=t$0 // validation marker ``` `$2` gets assigned a reactive scope. But `t$0` is never mutated after the reassignment (note that this is impossible by construction -- `validatePreserveExistingMemo` would fail earlier in inferReferenceEffects in that case), so it would never receive a scope. We incorrectly check `t$0` (instead of `$2`) for scope pruning **This PR** This PR fixes a bug around ValidatePreserveUseMemo + iife inlining. For useMemos that are successfully inlined, we previously always hit case 1: assuming that useMemo return values don't need reactive scopes due to type inference. This PR patches `validatePreserveExistingMemo` to check through reassignments to the `FinishMemoize` value. Since FinishMemoize only ever records rvalues (the result of the useMemo / useCallback call itself), this should only affect inlined useMemos. Following the same example, we previously would check that `t$0` preserves existing memo semantics. Now, we check `$2` instead. TODO: test by syncing internally [ghstack-poisoned]
ghstack-source-id: b9c13bf5f858123b68c9e89ca8c7629cf2b90a15 Pull Request resolved: #30603
if (instruction.value.kind === 'FinishMemoize') { | ||
const identifier = instruction.value.decl.identifier; | ||
const value = instruction.value; | ||
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are other forms of reassignment, should we handle Destructure, PostfixUpdate, and PrefixUpdate too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is intended to only map inlined iife return values, which we replace with a reassignment to a generated + promoted temporary.
This is why the affected test cases are all useMemo
related
useHook(); | ||
x.push(props); | ||
|
||
return useMemo(() => [x], [x]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense to add special-casing for pruning of always invalidating scopes to avoid this false positive
ghstack-source-id: b9c13bf5f858123b68c9e89ca8c7629cf2b90a15 Pull Request resolved: #30603
Stack from ghstack (oldest at bottom):
Background
ValidatePreserveExistingMemo checks that useMemos are preserved by checking that the returned result of the existing
useMemo
/useCallbacks
either:We prune scopes for a few reasons. This is a bit overly conservative as there are valid reasons for pruning (see special-casing for
PruneAlwaysInvalidatingScopes
). Note that this bugfix actually exposes some of these. We can either special-case more pruning passes to be valid (potentially introducingPrunedScope.reason
) or continue to be conservative to retain source semantics.The bug
When inlining useMemo, we convert the useMemo to an iife and inline it, synthesizing a temporary variable for recording the iife return value.
$2
gets assigned a reactive scope. Butt$0
is never mutated after the reassignment (note that this is impossible by construction --validatePreserveExistingMemo
would fail earlier in inferReferenceEffects in that case), so it would never receive a scope. We incorrectly checkt$0
(instead of$2
) for scope pruningThis PR
This PR fixes a bug around ValidatePreserveUseMemo + iife inlining. For useMemos that are successfully inlined, we previously always hit case 1: assuming that useMemo return values don't need reactive scopes due to type inference.
This PR patches
validatePreserveExistingMemo
to check through reassignments to theFinishMemoize
value. Since FinishMemoize only ever records rvalues (the result of the useMemo / useCallback call itself), this should only affect inlined useMemos.Following the same example, we previously would check that
t$0
preserves existing memo semantics. Now, we check$2
instead.