Skip to content

Commit a45f59d

Browse files
committed
[compiler][rfc] enablePreserveMemo treats manual deps as non-nullable
The `@enablePreserveExistingMemoizationGuarantees` mode can still fail to preserve manual memoization due to mismtached dependencies. Specifically, where the user's dependencies are more precise than the compiler infers bc the compiler is being conservative about what might be nullable. In this mode though we're intentionally using information from the manual memoization and can also rely on the deps as a signal for what's non-nullable. The idea of the PR — which I need to test with optional chains still — is that we treat manual memo deps just like other inferred-as-non-nullable objects during PropagateScopeDeps. We're careful to not treat the full path as non-nullable, only up to the last property index. So `x.y.z` as a manual dep treats `x.y` as non-nullable, allowing us to preserve a conditional dependency on `x.y.z`. I still need to handle optionals and think about what should happen for manual deps like `x?.y?.z`.
1 parent 9645803 commit a45f59d

File tree

5 files changed

+325
-0
lines changed

5 files changed

+325
-0
lines changed

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
ScopeId,
3434
TInstruction,
3535
} from './HIR';
36+
import {printManualMemoDependency} from './PrintHIR';
3637

3738
const DEBUG_PRINT = false;
3839

@@ -454,6 +455,25 @@ function collectNonNullsInBlocks(
454455
assumedNonNullObjects.add(entry);
455456
}
456457
}
458+
} else if (
459+
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
460+
instr.value.kind === 'StartMemoize' &&
461+
instr.value.deps != null
462+
) {
463+
for (const dep of instr.value.deps) {
464+
if (dep.root.kind === 'NamedLocal') {
465+
const depNode = context.registry.getOrCreateProperty({
466+
identifier: dep.root.value.identifier,
467+
path: dep.path.slice(0, -1),
468+
reactive: dep.root.value.reactive,
469+
});
470+
if (
471+
isImmutableAtInstr(depNode.fullPath.identifier, instr.id, context)
472+
) {
473+
assumedNonNullObjects.add(depNode);
474+
}
475+
}
476+
}
457477
}
458478
}
459479

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
6+
7+
import {useMemo} from 'react';
8+
import {identity, ValidateMemoization} from 'shared-runtime';
9+
10+
function Component({x}) {
11+
const object = useMemo(() => {
12+
return identity({
13+
callback: () => {
14+
return identity(x.y.z); // accesses more levels of properties than the manual memo
15+
},
16+
});
17+
// x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
18+
// we can only take a dep on x.y, not x.y.z
19+
}, [x.y]);
20+
const result = useMemo(() => {
21+
return [object.callback()];
22+
}, [object]);
23+
return <ValidateMemoization inputs={[x.y]} output={result} />;
24+
}
25+
26+
const input1 = {x: {y: {z: 42}}};
27+
const input1b = {x: {y: {z: 42}}};
28+
const input2 = {x: {y: {z: 3.14}}};
29+
export const FIXTURE_ENTRYPOINT = {
30+
fn: Component,
31+
params: [input1],
32+
sequentialRenders: [
33+
input1,
34+
input1,
35+
input1b, // should reset even though .z didn't change
36+
input1,
37+
input2,
38+
],
39+
};
40+
41+
```
42+
43+
## Code
44+
45+
```javascript
46+
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
47+
48+
import { useMemo } from "react";
49+
import { identity, ValidateMemoization } from "shared-runtime";
50+
51+
function Component(t0) {
52+
const $ = _c(11);
53+
const { x } = t0;
54+
let t1;
55+
if ($[0] !== x.y) {
56+
t1 = identity({ callback: () => identity(x.y.z) });
57+
$[0] = x.y;
58+
$[1] = t1;
59+
} else {
60+
t1 = $[1];
61+
}
62+
const object = t1;
63+
let t2;
64+
if ($[2] !== object) {
65+
t2 = object.callback();
66+
$[2] = object;
67+
$[3] = t2;
68+
} else {
69+
t2 = $[3];
70+
}
71+
let t3;
72+
if ($[4] !== t2) {
73+
t3 = [t2];
74+
$[4] = t2;
75+
$[5] = t3;
76+
} else {
77+
t3 = $[5];
78+
}
79+
const result = t3;
80+
let t4;
81+
if ($[6] !== x.y) {
82+
t4 = [x.y];
83+
$[6] = x.y;
84+
$[7] = t4;
85+
} else {
86+
t4 = $[7];
87+
}
88+
let t5;
89+
if ($[8] !== result || $[9] !== t4) {
90+
t5 = <ValidateMemoization inputs={t4} output={result} />;
91+
$[8] = result;
92+
$[9] = t4;
93+
$[10] = t5;
94+
} else {
95+
t5 = $[10];
96+
}
97+
return t5;
98+
}
99+
100+
const input1 = { x: { y: { z: 42 } } };
101+
const input1b = { x: { y: { z: 42 } } };
102+
const input2 = { x: { y: { z: 3.14 } } };
103+
export const FIXTURE_ENTRYPOINT = {
104+
fn: Component,
105+
params: [input1],
106+
sequentialRenders: [
107+
input1,
108+
input1,
109+
input1b, // should reset even though .z didn't change
110+
input1,
111+
input2,
112+
],
113+
};
114+
115+
```
116+
117+
### Eval output
118+
(kind: ok) <div>{"inputs":[{"z":42}],"output":[42]}</div>
119+
<div>{"inputs":[{"z":42}],"output":[42]}</div>
120+
<div>{"inputs":[{"z":42}],"output":[42]}</div>
121+
<div>{"inputs":[{"z":42}],"output":[42]}</div>
122+
<div>{"inputs":[{"z":3.14}],"output":[3.14]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
2+
3+
import {useMemo} from 'react';
4+
import {identity, ValidateMemoization} from 'shared-runtime';
5+
6+
function Component({x}) {
7+
const object = useMemo(() => {
8+
return identity({
9+
callback: () => {
10+
return identity(x.y.z); // accesses more levels of properties than the manual memo
11+
},
12+
});
13+
// x.y as a manual dep only tells us that x is non-nullable, not that x.y is non-nullable
14+
// we can only take a dep on x.y, not x.y.z
15+
}, [x.y]);
16+
const result = useMemo(() => {
17+
return [object.callback()];
18+
}, [object]);
19+
return <ValidateMemoization inputs={[x.y]} output={result} />;
20+
}
21+
22+
const input1 = {x: {y: {z: 42}}};
23+
const input1b = {x: {y: {z: 42}}};
24+
const input2 = {x: {y: {z: 3.14}}};
25+
export const FIXTURE_ENTRYPOINT = {
26+
fn: Component,
27+
params: [input1],
28+
sequentialRenders: [
29+
input1,
30+
input1,
31+
input1b, // should reset even though .z didn't change
32+
input1,
33+
input2,
34+
],
35+
};
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
6+
7+
import {useMemo} from 'react';
8+
import {identity, ValidateMemoization} from 'shared-runtime';
9+
10+
function Component({x}) {
11+
const object = useMemo(() => {
12+
return identity({
13+
callback: () => {
14+
return identity(x.y.z);
15+
},
16+
});
17+
}, [x.y.z]);
18+
const result = useMemo(() => {
19+
return [object.callback()];
20+
}, [object]);
21+
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
22+
}
23+
24+
export const FIXTURE_ENTRYPOINT = {
25+
fn: Component,
26+
params: [{x: {y: {z: 42}}}],
27+
sequentialRenders: [
28+
{x: {y: {z: 42}}},
29+
{x: {y: {z: 42}}},
30+
{x: {y: {z: 3.14}}},
31+
{x: {y: {z: 42}}},
32+
{x: {y: {z: 3.14}}},
33+
{x: {y: {z: 42}}},
34+
],
35+
};
36+
37+
```
38+
39+
## Code
40+
41+
```javascript
42+
import { c as _c } from "react/compiler-runtime"; // @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
43+
44+
import { useMemo } from "react";
45+
import { identity, ValidateMemoization } from "shared-runtime";
46+
47+
function Component(t0) {
48+
const $ = _c(11);
49+
const { x } = t0;
50+
let t1;
51+
if ($[0] !== x.y.z) {
52+
t1 = identity({ callback: () => identity(x.y.z) });
53+
$[0] = x.y.z;
54+
$[1] = t1;
55+
} else {
56+
t1 = $[1];
57+
}
58+
const object = t1;
59+
let t2;
60+
if ($[2] !== object) {
61+
t2 = object.callback();
62+
$[2] = object;
63+
$[3] = t2;
64+
} else {
65+
t2 = $[3];
66+
}
67+
let t3;
68+
if ($[4] !== t2) {
69+
t3 = [t2];
70+
$[4] = t2;
71+
$[5] = t3;
72+
} else {
73+
t3 = $[5];
74+
}
75+
const result = t3;
76+
let t4;
77+
if ($[6] !== x.y.z) {
78+
t4 = [x.y.z];
79+
$[6] = x.y.z;
80+
$[7] = t4;
81+
} else {
82+
t4 = $[7];
83+
}
84+
let t5;
85+
if ($[8] !== result || $[9] !== t4) {
86+
t5 = <ValidateMemoization inputs={t4} output={result} />;
87+
$[8] = result;
88+
$[9] = t4;
89+
$[10] = t5;
90+
} else {
91+
t5 = $[10];
92+
}
93+
return t5;
94+
}
95+
96+
export const FIXTURE_ENTRYPOINT = {
97+
fn: Component,
98+
params: [{ x: { y: { z: 42 } } }],
99+
sequentialRenders: [
100+
{ x: { y: { z: 42 } } },
101+
{ x: { y: { z: 42 } } },
102+
{ x: { y: { z: 3.14 } } },
103+
{ x: { y: { z: 42 } } },
104+
{ x: { y: { z: 3.14 } } },
105+
{ x: { y: { z: 42 } } },
106+
],
107+
};
108+
109+
```
110+
111+
### Eval output
112+
(kind: ok) <div>{"inputs":[42],"output":[42]}</div>
113+
<div>{"inputs":[42],"output":[42]}</div>
114+
<div>{"inputs":[3.14],"output":[3.14]}</div>
115+
<div>{"inputs":[42],"output":[42]}</div>
116+
<div>{"inputs":[3.14],"output":[3.14]}</div>
117+
<div>{"inputs":[42],"output":[42]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @enablePreserveExistingMemoizationGuarantees @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enableTreatFunctionDepsAsConditional:false
2+
3+
import {useMemo} from 'react';
4+
import {identity, ValidateMemoization} from 'shared-runtime';
5+
6+
function Component({x}) {
7+
const object = useMemo(() => {
8+
return identity({
9+
callback: () => {
10+
return identity(x.y.z);
11+
},
12+
});
13+
}, [x.y.z]);
14+
const result = useMemo(() => {
15+
return [object.callback()];
16+
}, [object]);
17+
return <ValidateMemoization inputs={[x.y.z]} output={result} />;
18+
}
19+
20+
export const FIXTURE_ENTRYPOINT = {
21+
fn: Component,
22+
params: [{x: {y: {z: 42}}}],
23+
sequentialRenders: [
24+
{x: {y: {z: 42}}},
25+
{x: {y: {z: 42}}},
26+
{x: {y: {z: 3.14}}},
27+
{x: {y: {z: 42}}},
28+
{x: {y: {z: 3.14}}},
29+
{x: {y: {z: 42}}},
30+
],
31+
};

0 commit comments

Comments
 (0)