-
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
Changes from 3 commits
2e123c2
fda8406
52d1dcf
9a259b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ import {CompilerError} from '../CompilerError'; | |
import { | ||
DeclarationId, | ||
Environment, | ||
Identifier, | ||
InstructionId, | ||
Pattern, | ||
Place, | ||
|
@@ -24,7 +25,7 @@ import { | |
isMutableEffect, | ||
} from '../HIR'; | ||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects'; | ||
import {assertExhaustive} from '../Utils/utils'; | ||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils'; | ||
import {getPlaceScope} from './BuildReactiveBlocks'; | ||
import { | ||
ReactiveFunctionTransform, | ||
|
@@ -935,6 +936,11 @@ class PruneScopesTransform extends ReactiveFunctionTransform< | |
Set<DeclarationId> | ||
> { | ||
prunedScopes: Set<ScopeId> = new Set(); | ||
/** | ||
* Track reassignments so we can correctly set `pruned` flags for | ||
* inlined useMemos. | ||
*/ | ||
reassignments: Map<DeclarationId, Set<Identifier>> = new Map(); | ||
|
||
override transformScope( | ||
scopeBlock: ReactiveScopeBlock, | ||
|
@@ -977,24 +983,45 @@ class PruneScopesTransform extends ReactiveFunctionTransform< | |
} | ||
} | ||
|
||
/** | ||
* If we pruned the scope for a non-escaping value, we know it doesn't | ||
* need to be memoized. Remove associated `Memoize` instructions so that | ||
* we don't report false positives on "missing" memoization of these values. | ||
*/ | ||
override transformInstruction( | ||
instruction: ReactiveInstruction, | ||
state: Set<DeclarationId>, | ||
): Transformed<ReactiveStatement> { | ||
this.traverseInstruction(instruction, state); | ||
|
||
/** | ||
* If we pruned the scope for a non-escaping value, we know it doesn't | ||
* need to be memoized. Remove associated `Memoize` instructions so that | ||
* we don't report false positives on "missing" memoization of these values. | ||
*/ | ||
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 commentThe 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 commentThe 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 |
||
const ids = getOrInsertDefault( | ||
this.reassignments, | ||
value.lvalue.place.identifier.declarationId, | ||
new Set(), | ||
); | ||
ids.add(value.value.identifier); | ||
} else if (value.kind === 'FinishMemoize') { | ||
let decls; | ||
if (value.decl.identifier.scope == null) { | ||
/** | ||
* If the manual memo was a useMemo that got inlined, iterate through | ||
* all reassignments to the iife temporary to ensure they're memoized. | ||
*/ | ||
decls = this.reassignments.get(value.decl.identifier.declarationId) ?? [ | ||
value.decl.identifier, | ||
]; | ||
} else { | ||
decls = [value.decl.identifier]; | ||
} | ||
|
||
if ( | ||
identifier.scope !== null && | ||
this.prunedScopes.has(identifier.scope.id) | ||
[...decls].every( | ||
decl => decl.scope == null || this.prunedScopes.has(decl.scope.id), | ||
) | ||
) { | ||
instruction.value.pruned = true; | ||
value.pruned = true; | ||
Comment on lines
+1020
to
+1024
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See newly added test fixtures for why this is needed |
||
} | ||
} | ||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @validatePreserveExistingMemoizationGuarantees | ||
|
||
import {useMemo} from 'react'; | ||
import {useHook} from 'shared-runtime'; | ||
|
||
// useMemo values may not be memoized in Forget output if we | ||
// infer that their deps always invalidate. | ||
// This is technically a false positive as the useMemo in source | ||
// was effectively a no-op | ||
function useFoo(props) { | ||
const x = []; | ||
useHook(); | ||
x.push(props); | ||
|
||
return useMemo(() => [x], [x]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 |
||
} | ||
|
||
export const FIXTURE_ENTRYPOINT = { | ||
fn: useFoo, | ||
params: [{}], | ||
}; | ||
|
||
``` | ||
|
||
|
||
## Error | ||
|
||
``` | ||
13 | x.push(props); | ||
14 | | ||
> 15 | return useMemo(() => [x], [x]); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (15:15) | ||
16 | } | ||
17 | | ||
18 | export const FIXTURE_ENTRYPOINT = { | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @validatePreserveExistingMemoizationGuarantees:true | ||
|
||
import {useRef, useMemo} from 'react'; | ||
import {makeArray} from 'shared-runtime'; | ||
|
||
function useFoo() { | ||
const r = useRef(); | ||
return useMemo(() => makeArray(r), []); | ||
} | ||
|
||
export const FIXTURE_ENTRYPOINT = { | ||
fn: useFoo, | ||
params: [], | ||
}; | ||
|
||
``` | ||
|
||
|
||
## Error | ||
|
||
``` | ||
6 | function useFoo() { | ||
7 | const r = useRef(); | ||
> 8 | return useMemo(() => makeArray(r), []); | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (8:8) | ||
9 | } | ||
10 | | ||
11 | export const FIXTURE_ENTRYPOINT = { | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
|
||
## Input | ||
|
||
```javascript | ||
// @validatePreserveExistingMemoizationGuarantees | ||
|
||
import {useMemo} from 'react'; | ||
import {identity} from 'shared-runtime'; | ||
|
||
function useFoo(cond) { | ||
useMemo(() => { | ||
if (cond) { | ||
return 2; | ||
} else { | ||
return identity(5); | ||
} | ||
}, [cond]); | ||
} | ||
|
||
export const FIXTURE_ENTRYPOINT = { | ||
fn: useFoo, | ||
params: [true], | ||
}; | ||
|
||
``` | ||
|
||
## Code | ||
|
||
```javascript | ||
// @validatePreserveExistingMemoizationGuarantees | ||
|
||
import { useMemo } from "react"; | ||
import { identity } from "shared-runtime"; | ||
|
||
function useFoo(cond) { | ||
let t0; | ||
if (cond) { | ||
t0 = 2; | ||
} else { | ||
t0 = identity(5); | ||
} | ||
} | ||
|
||
export const FIXTURE_ENTRYPOINT = { | ||
fn: useFoo, | ||
params: [true], | ||
}; | ||
|
||
``` | ||
|
||
### Eval output | ||
(kind: ok) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// @validatePreserveExistingMemoizationGuarantees | ||
|
||
import {useMemo} from 'react'; | ||
import {identity} from 'shared-runtime'; | ||
|
||
function useFoo(cond) { | ||
useMemo(() => { | ||
if (cond) { | ||
return 2; | ||
} else { | ||
return identity(5); | ||
} | ||
}, [cond]); | ||
} | ||
|
||
export const FIXTURE_ENTRYPOINT = { | ||
fn: useFoo, | ||
params: [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.
This is the same logic (and comments) as in ValidatePreserveManualMemo. This was needed to avoid a regression of 0c86667