Skip to content

Commit 6772dc0

Browse files
committed
[compiler][wip] Fix for ValidatePreserveMemo edge case w multiple return in useMemo
Potential fix for #34750. With a useMemo with multiple returns we can sometimes end up thinking the FinishMemoize value wasn't memoized, even though it definitely is. But the same case works if you rewrite to have a let binding, assign to the let instead of returning, and then have a single return with the let — which is the exact shape we convert to when we inline the useMemo! The only difference is that when we inline, we end up with an extra LoadLocal. Adding an extra LoadLocal "fixes" the issue but this is definitely a hack. However, along the way this helped me find that we also have a bug in PruneAlwaysInvalidatingScopes, where we were forgetting to set FinishMemoize instructions to pruned if their declaration always invalidates. Putting up for feedback, not necessarily expecting to land as-is.
1 parent 1895bec commit 6772dc0

10 files changed

+260
-107
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ export function createTemporaryPlace(
988988
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
989989
reactive: false,
990990
effect: Effect.Unknown,
991-
loc: GeneratedSource,
991+
loc,
992992
};
993993
}
994994

compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
CallExpression,
1212
Effect,
1313
Environment,
14-
FinishMemoize,
1514
FunctionExpression,
1615
HIRFunction,
1716
IdentifierId,
@@ -25,7 +24,6 @@ import {
2524
Place,
2625
PropertyLoad,
2726
SpreadPattern,
28-
StartMemoize,
2927
TInstruction,
3028
getHookKindForType,
3129
makeInstructionId,
@@ -184,36 +182,52 @@ function makeManualMemoizationMarkers(
184182
depsList: Array<ManualMemoDependency> | null,
185183
memoDecl: Place,
186184
manualMemoId: number,
187-
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
185+
): [Array<Instruction>, Array<Instruction>] {
186+
const temp = createTemporaryPlace(env, memoDecl.loc);
188187
return [
189-
{
190-
id: makeInstructionId(0),
191-
lvalue: createTemporaryPlace(env, fnExpr.loc),
192-
value: {
193-
kind: 'StartMemoize',
194-
manualMemoId,
195-
/*
196-
* Use deps list from source instead of inferred deps
197-
* as dependencies
198-
*/
199-
deps: depsList,
188+
[
189+
{
190+
id: makeInstructionId(0),
191+
lvalue: createTemporaryPlace(env, fnExpr.loc),
192+
value: {
193+
kind: 'StartMemoize',
194+
manualMemoId,
195+
/*
196+
* Use deps list from source instead of inferred deps
197+
* as dependencies
198+
*/
199+
deps: depsList,
200+
loc: fnExpr.loc,
201+
},
202+
effects: null,
200203
loc: fnExpr.loc,
201204
},
202-
effects: null,
203-
loc: fnExpr.loc,
204-
},
205-
{
206-
id: makeInstructionId(0),
207-
lvalue: createTemporaryPlace(env, fnExpr.loc),
208-
value: {
209-
kind: 'FinishMemoize',
210-
manualMemoId,
211-
decl: {...memoDecl},
212-
loc: fnExpr.loc,
205+
],
206+
[
207+
{
208+
id: makeInstructionId(0),
209+
lvalue: {...temp},
210+
value: {
211+
kind: 'LoadLocal',
212+
place: {...memoDecl},
213+
loc: memoDecl.loc,
214+
},
215+
effects: null,
216+
loc: memoDecl.loc,
217+
},
218+
{
219+
id: makeInstructionId(0),
220+
lvalue: createTemporaryPlace(env, memoDecl.loc),
221+
value: {
222+
kind: 'FinishMemoize',
223+
manualMemoId,
224+
decl: {...temp},
225+
loc: memoDecl.loc,
226+
},
227+
effects: null,
228+
loc: memoDecl.loc,
213229
},
214-
effects: null,
215-
loc: fnExpr.loc,
216-
},
230+
],
217231
];
218232
}
219233

@@ -409,10 +423,7 @@ export function dropManualMemoization(
409423
* LoadLocal fnArg
410424
* - (if validation is enabled) collect manual memoization markers
411425
*/
412-
const queuedInserts: Map<
413-
InstructionId,
414-
TInstruction<StartMemoize> | TInstruction<FinishMemoize>
415-
> = new Map();
426+
const queuedInserts: Map<InstructionId, Array<Instruction>> = new Map();
416427
for (const [_, block] of func.body.blocks) {
417428
for (let i = 0; i < block.instructions.length; i++) {
418429
const instr = block.instructions[i]!;
@@ -557,11 +568,11 @@ export function dropManualMemoization(
557568
let nextInstructions: Array<Instruction> | null = null;
558569
for (let i = 0; i < block.instructions.length; i++) {
559570
const instr = block.instructions[i];
560-
const insertInstr = queuedInserts.get(instr.id);
561-
if (insertInstr != null) {
571+
const insertInstructions = queuedInserts.get(instr.id);
572+
if (insertInstructions != null) {
562573
nextInstructions = nextInstructions ?? block.instructions.slice(0, i);
563574
nextInstructions.push(instr);
564-
nextInstructions.push(insertInstr);
575+
nextInstructions.push(...insertInstructions);
565576
} else if (nextInstructions != null) {
566577
nextInstructions.push(instr);
567578
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,15 @@ class Transform extends ReactiveFunctionTransform<boolean> {
7777
}
7878
break;
7979
}
80+
case 'FinishMemoize': {
81+
if (
82+
!withinScope &&
83+
this.alwaysInvalidatingValues.has(value.decl.identifier)
84+
) {
85+
value.pruned = true;
86+
}
87+
break;
88+
}
8089
}
8190
return {kind: 'keep'};
8291
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Found 1 error:
2929
3030
Invariant: Expected consistent kind for destructuring
3131
32-
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
32+
Other places were `Reassign` but 'mutate? #t8$47[7:9]{reactive}' is const.
3333
3434
error.bug-invariant-expected-consistent-destructuring.ts:9:9
3535
7 |

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

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

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-dropped-infer-always-invalidating.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
7+
import {useMemo} from 'react';
8+
import {useHook} from 'shared-runtime';
9+
10+
// If we can prove that a useMemo was ineffective because it would always invalidate,
11+
// then we shouldn't throw a "couldn't preserve existing memoization" error
12+
// TODO: consider reporting a separate error to the user for this case, if you're going
13+
// to memoize manually, then you probably want to know that it's 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+
## Code
30+
31+
```javascript
32+
// @validatePreserveExistingMemoizationGuarantees
33+
34+
import { useMemo } from "react";
35+
import { useHook } from "shared-runtime";
36+
37+
// If we can prove that a useMemo was ineffective because it would always invalidate,
38+
// then we shouldn't throw a "couldn't preserve existing memoization" error
39+
// TODO: consider reporting a separate error to the user for this case, if you're going
40+
// to memoize manually, then you probably want to know that it's a no-op
41+
function useFoo(props) {
42+
const x = [];
43+
useHook();
44+
x.push(props);
45+
return [x];
46+
}
47+
48+
export const FIXTURE_ENTRYPOINT = {
49+
fn: useFoo,
50+
params: [{}],
51+
};
52+
53+
```
54+
55+
### Eval output
56+
(kind: ok) [[{}]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
3+
import {useMemo} from 'react';
4+
import {useHook} from 'shared-runtime';
5+
6+
// If we can prove that a useMemo was ineffective because it would always invalidate,
7+
// then we shouldn't throw a "couldn't preserve existing memoization" error
8+
// TODO: consider reporting a separate error to the user for this case, if you're going
9+
// to memoize manually, then you probably want to know that it's a no-op
10+
function useFoo(props) {
11+
const x = [];
12+
useHook();
13+
x.push(props);
14+
15+
return useMemo(() => [x], [x]);
16+
}
17+
18+
export const FIXTURE_ENTRYPOINT = {
19+
fn: useFoo,
20+
params: [{}],
21+
};

0 commit comments

Comments
 (0)