Skip to content

Commit 405e79e

Browse files
committed
[compiler] Patch ValidatePreserveMemo to bailout correctly for refs
ghstack-source-id: 73fc47f Pull Request resolved: #30603
1 parent cf6bfd6 commit 405e79e

14 files changed

+373
-125
lines changed

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {CompilerError} from '../CompilerError';
99
import {
1010
DeclarationId,
1111
Environment,
12+
Identifier,
1213
InstructionId,
1314
Pattern,
1415
Place,
@@ -24,7 +25,7 @@ import {
2425
isMutableEffect,
2526
} from '../HIR';
2627
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
27-
import {assertExhaustive} from '../Utils/utils';
28+
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
2829
import {getPlaceScope} from './BuildReactiveBlocks';
2930
import {
3031
ReactiveFunctionTransform,
@@ -935,6 +936,11 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
935936
Set<DeclarationId>
936937
> {
937938
prunedScopes: Set<ScopeId> = new Set();
939+
/**
940+
* Track reassignments so we can correctly set `pruned` flags for
941+
* inlined useMemos.
942+
*/
943+
reassignments: Map<DeclarationId, Set<Identifier>> = new Map();
938944

939945
override transformScope(
940946
scopeBlock: ReactiveScopeBlock,
@@ -977,24 +983,45 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
977983
}
978984
}
979985

986+
/**
987+
* If we pruned the scope for a non-escaping value, we know it doesn't
988+
* need to be memoized. Remove associated `Memoize` instructions so that
989+
* we don't report false positives on "missing" memoization of these values.
990+
*/
980991
override transformInstruction(
981992
instruction: ReactiveInstruction,
982993
state: Set<DeclarationId>,
983994
): Transformed<ReactiveStatement> {
984995
this.traverseInstruction(instruction, state);
985996

986-
/**
987-
* If we pruned the scope for a non-escaping value, we know it doesn't
988-
* need to be memoized. Remove associated `Memoize` instructions so that
989-
* we don't report false positives on "missing" memoization of these values.
990-
*/
991-
if (instruction.value.kind === 'FinishMemoize') {
992-
const identifier = instruction.value.decl.identifier;
997+
const value = instruction.value;
998+
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
999+
const ids = getOrInsertDefault(
1000+
this.reassignments,
1001+
value.lvalue.place.identifier.declarationId,
1002+
new Set(),
1003+
);
1004+
ids.add(value.value.identifier);
1005+
} else if (value.kind === 'FinishMemoize') {
1006+
let decls;
1007+
if (value.decl.identifier.scope == null) {
1008+
/**
1009+
* If the manual memo was a useMemo that got inlined, iterate through
1010+
* all reassignments to the iife temporary to ensure they're memoized.
1011+
*/
1012+
decls = this.reassignments.get(value.decl.identifier.declarationId) ?? [
1013+
value.decl.identifier,
1014+
];
1015+
} else {
1016+
decls = [value.decl.identifier];
1017+
}
1018+
9931019
if (
994-
identifier.scope !== null &&
995-
this.prunedScopes.has(identifier.scope.id)
1020+
[...decls].every(
1021+
decl => decl.scope == null || this.prunedScopes.has(decl.scope.id),
1022+
)
9961023
) {
997-
instruction.value.pruned = true;
1024+
value.pruned = true;
9981025
}
9991026
}
10001027

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

Lines changed: 47 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

@@ -434,6 +445,18 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
434445
*/
435446
this.recordTemporaries(instruction, state);
436447
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+
}
437460
if (value.kind === 'StartMemoize') {
438461
let depsFromSource: Array<ManualMemoDependency> | null = null;
439462
if (value.deps != null) {
@@ -451,6 +474,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
451474
decls: new Set(),
452475
depsFromSource,
453476
manualMemoId: value.manualMemoId,
477+
reassignments: new Map(),
454478
};
455479

456480
for (const {identifier, loc} of eachInstructionValueOperand(
@@ -483,20 +507,34 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
483507
suggestions: null,
484508
},
485509
);
510+
const reassignments = state.manualMemoState.reassignments;
486511
state.manualMemoState = null;
487512
if (!value.pruned) {
488513
for (const {identifier, loc} of eachInstructionValueOperand(
489514
value as InstructionValue,
490515
)) {
491-
if (isUnmemoized(identifier, this.scopes)) {
492-
state.errors.push({
493-
reason:
494-
'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.',
495-
description: null,
496-
severity: ErrorSeverity.CannotPreserveMemoization,
497-
loc,
498-
suggestions: null,
499-
});
516+
let decls;
517+
if (identifier.scope == null) {
518+
/**
519+
* If the manual memo was a useMemo that got inlined, iterate through
520+
* all reassignments to the iife temporary to ensure they're memoized.
521+
*/
522+
decls = reassignments.get(identifier.declarationId) ?? [identifier];
523+
} else {
524+
decls = [identifier];
525+
}
526+
527+
for (const identifier of decls) {
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+
}
500538
}
501539
}
502540
}

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.
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();
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+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
7+
import {useMemo} from 'react';
8+
import {identity} from 'shared-runtime';
9+
10+
function useFoo(cond) {
11+
useMemo(() => {
12+
if (cond) {
13+
return 2;
14+
} else {
15+
return identity(5);
16+
}
17+
}, [cond]);
18+
}
19+
20+
export const FIXTURE_ENTRYPOINT = {
21+
fn: useFoo,
22+
params: [true],
23+
};
24+
25+
```
26+
27+
## Code
28+
29+
```javascript
30+
// @validatePreserveExistingMemoizationGuarantees
31+
32+
import { useMemo } from "react";
33+
import { identity } from "shared-runtime";
34+
35+
function useFoo(cond) {
36+
let t0;
37+
if (cond) {
38+
t0 = 2;
39+
} else {
40+
t0 = identity(5);
41+
}
42+
}
43+
44+
export const FIXTURE_ENTRYPOINT = {
45+
fn: useFoo,
46+
params: [true],
47+
};
48+
49+
```
50+
51+
### Eval output
52+
(kind: ok)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
3+
import {useMemo} from 'react';
4+
import {identity} from 'shared-runtime';
5+
6+
function useFoo(cond) {
7+
useMemo(() => {
8+
if (cond) {
9+
return 2;
10+
} else {
11+
return identity(5);
12+
}
13+
}, [cond]);
14+
}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: useFoo,
18+
params: [true],
19+
};

0 commit comments

Comments
 (0)