Skip to content

Commit 7b6be39

Browse files
committed
[compiler] Patch ValidatePreserveMemo to bailout correctly for refs
ghstack-source-id: 3f5d5c1 Pull Request resolved: #30603
1 parent 90f17cd commit 7b6be39

9 files changed

+182
-114
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ReactiveFunctionVisitor,
3131
visitReactiveFunction,
3232
} from '../ReactiveScopes/visitors';
33+
import {getOrInsertDefault} from '../Utils/utils';
3334

3435
/**
3536
* Validates that all explicit manual memoization (useMemo/useCallback) was accurately
@@ -52,6 +53,16 @@ export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
5253
const DEBUG = false;
5354

5455
type ManualMemoBlockState = {
56+
/**
57+
* Tracks reassigned temporaries.
58+
* This is necessary because useMemo calls are usually inlined.
59+
* Inlining produces a `let` declaration, followed by reassignments
60+
* to the newly declared variable (one per return statement).
61+
* Since InferReactiveScopes does not merge scopes across reassigned
62+
* variables (except in the case of a mutate-after-phi), we need to
63+
* track reassignments to validate we're retaining manual memo.
64+
*/
65+
reassignments: Map<DeclarationId, Set<Identifier>>;
5566
// The source of the original memoization, used when reporting errors
5667
loc: SourceLocation;
5768

@@ -433,6 +444,19 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
433444
* recursively visits ReactiveValues and instructions
434445
*/
435446
this.recordTemporaries(instruction, state);
447+
const value = instruction.value;
448+
if (
449+
value.kind === 'StoreLocal' &&
450+
value.lvalue.kind === 'Reassign' &&
451+
state.manualMemoState != null
452+
) {
453+
const ids = getOrInsertDefault(
454+
state.manualMemoState.reassignments,
455+
value.lvalue.place.identifier.declarationId,
456+
new Set(),
457+
);
458+
ids.add(value.value.identifier);
459+
}
436460
if (instruction.value.kind === 'StartMemoize') {
437461
let depsFromSource: Array<ManualMemoDependency> | null = null;
438462
if (instruction.value.deps != null) {
@@ -450,6 +474,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
450474
decls: new Set(),
451475
depsFromSource,
452476
manualMemoId: instruction.value.manualMemoId,
477+
reassignments: new Map(),
453478
};
454479

455480
for (const {identifier, loc} of eachInstructionValueOperand(
@@ -482,20 +507,34 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
482507
suggestions: null,
483508
},
484509
);
510+
const reassignments = state.manualMemoState.reassignments;
485511
state.manualMemoState = null;
486512
if (!instruction.value.pruned) {
487513
for (const {identifier, loc} of eachInstructionValueOperand(
488514
instruction.value as InstructionValue,
489515
)) {
490-
if (isUnmemoized(identifier, this.scopes)) {
491-
state.errors.push({
492-
reason:
493-
'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.',
494-
description: null,
495-
severity: ErrorSeverity.CannotPreserveMemoization,
496-
loc,
497-
suggestions: null,
498-
});
516+
let manualMemoRvals;
517+
if (identifier.scope == null) {
518+
// If the manual memo was a useMemo that got inlined, iterate through
519+
// all reassignments to the iife temporary to ensure they're memoized.
520+
manualMemoRvals = reassignments.get(identifier.declarationId) ?? [
521+
identifier,
522+
];
523+
} else {
524+
manualMemoRvals = [identifier];
525+
}
526+
527+
for (const identifier of manualMemoRvals) {
528+
if (isUnmemoized(identifier, this.scopes)) {
529+
state.errors.push({
530+
reason:
531+
'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.',
532+
description: null,
533+
severity: ErrorSeverity.CannotPreserveMemoization,
534+
loc,
535+
suggestions: null,
536+
});
537+
}
499538
}
500539
}
501540
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-maybe-mutable-ref-memo-not-preserved.expect.md

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
7+
import {useMemo} from 'react';
8+
import {identity} from 'shared-runtime';
9+
10+
/**
11+
* This is technically a false positive, although it makes sense
12+
* to bailout as source code might be doing something sketchy.
13+
*/
14+
function useFoo(x) {
15+
useMemo(() => identity(x), [x]);
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: useFoo,
20+
params: [2],
21+
};
22+
23+
```
24+
25+
26+
## Error
27+
28+
```
29+
9 | */
30+
10 | function useFoo(x) {
31+
> 11 | useMemo(() => identity(x), [x]);
32+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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. (11:11)
33+
12 | }
34+
13 |
35+
14 | export const FIXTURE_ENTRYPOINT = {
36+
```
37+
38+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
3+
import {useMemo} from 'react';
4+
import {identity} from 'shared-runtime';
5+
6+
/**
7+
* This is technically a false positive, although it makes sense
8+
* to bailout as source code might be doing something sketchy.
9+
*/
10+
function useFoo(x) {
11+
useMemo(() => identity(x), [x]);
12+
}
13+
14+
export const FIXTURE_ENTRYPOINT = {
15+
fn: useFoo,
16+
params: [2],
17+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
7+
import {useMemo} from 'react';
8+
import {useHook} from 'shared-runtime';
9+
10+
// useMemo values may not be memoized in Forget output if we
11+
// infer that their deps always invalidate.
12+
// This is technically a false positive as the useMemo in source
13+
// was effectively a no-op
14+
function useFoo(props) {
15+
const x = [];
16+
useHook();
17+
x.push(props);
18+
19+
return useMemo(() => [x], [x]);
20+
}
21+
22+
export const FIXTURE_ENTRYPOINT = {
23+
fn: useFoo,
24+
params: [{}],
25+
};
26+
27+
```
28+
29+
30+
## Error
31+
32+
```
33+
13 | x.push(props);
34+
14 |
35+
> 15 | return useMemo(() => [x], [x]);
36+
| ^^^^^^^^^^^^^^^^^^^^^^^ 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)
37+
16 | }
38+
17 |
39+
18 | export const FIXTURE_ENTRYPOINT = {
40+
```
41+
42+
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {useHook} from 'shared-runtime';
55

66
// useMemo values may not be memoized in Forget output if we
77
// infer that their deps always invalidate.
8-
// This is still correct as the useMemo in source was effectively
9-
// a no-op already.
8+
// This is technically a false positive as the useMemo in source
9+
// was effectively a no-op
1010
function useFoo(props) {
1111
const x = [];
1212
useHook();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees:true
6+
7+
import {useRef, useMemo} from 'react';
8+
import {makeArray} from 'shared-runtime';
9+
10+
function useFoo() {
11+
const r = useRef();
12+
return useMemo(() => makeArray(r), []);
13+
}
14+
15+
export const FIXTURE_ENTRYPOINT = {
16+
fn: useFoo,
17+
params: [],
18+
};
19+
20+
```
21+
22+
23+
## Error
24+
25+
```
26+
6 | function useFoo() {
27+
7 | const r = useRef();
28+
> 8 | return useMemo(() => makeArray(r), []);
29+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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)
30+
9 | }
31+
10 |
32+
11 | export const FIXTURE_ENTRYPOINT = {
33+
```
34+
35+
File renamed without changes.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dropped-infer-always-invalidating.expect.md

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)