Skip to content

Commit dd2fccf

Browse files
committed
[compiler][hir] Only hoist always-accessed PropertyLoads from function decls
ghstack-source-id: c0a4b95 Pull Request resolved: #31066
1 parent e0425be commit dd2fccf

23 files changed

+942
-12
lines changed

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

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Set_union,
88
getOrInsertDefault,
99
} from '../Utils/utils';
10+
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
1011
import {
1112
BasicBlock,
1213
BlockId,
@@ -19,6 +20,7 @@ import {
1920
ReactiveScopeDependency,
2021
ScopeId,
2122
} from './HIR';
23+
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
2224

2325
/**
2426
* Helper function for `PropagateScopeDependencies`.
@@ -73,23 +75,38 @@ export function collectHoistablePropertyLoads(
7375
fn: HIRFunction,
7476
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
7577
optionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
76-
): ReadonlyMap<ScopeId, BlockInfo> {
78+
): ReadonlyMap<BlockId, BlockInfo> {
7779
const registry = new PropertyPathRegistry();
7880

79-
const nodes = collectNonNullsInBlocks(fn, temporaries, optionals, registry);
81+
const functionExpressionReferences = collectFunctionExpressionRValues(fn);
82+
const reallyAccessedTemporaries = new Map(
83+
[...temporaries].filter(([id]) => !functionExpressionReferences.has(id)),
84+
);
85+
const nodes = collectNonNullsInBlocks(
86+
fn,
87+
reallyAccessedTemporaries,
88+
optionals,
89+
registry,
90+
);
8091
propagateNonNull(fn, nodes, registry);
8192

82-
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
93+
return nodes;
94+
}
95+
96+
export function keyByScopeId<T>(
97+
fn: HIRFunction,
98+
source: ReadonlyMap<BlockId, T>,
99+
): ReadonlyMap<ScopeId, T> {
100+
const keyedByScopeId = new Map<ScopeId, T>();
83101
for (const [_, block] of fn.body.blocks) {
84102
if (block.terminal.kind === 'scope') {
85-
nodesKeyedByScopeId.set(
103+
keyedByScopeId.set(
86104
block.terminal.scope.id,
87-
nodes.get(block.terminal.block)!,
105+
source.get(block.terminal.block)!,
88106
);
89107
}
90108
}
91-
92-
return nodesKeyedByScopeId;
109+
return keyedByScopeId;
93110
}
94111

95112
export type BlockInfo = {
@@ -317,6 +334,27 @@ function collectNonNullsInBlocks(
317334
assumedNonNullObjects,
318335
);
319336
}
337+
} else if (
338+
instr.value.kind === 'FunctionExpression' &&
339+
!fn.env.config.enableTreatFunctionDepsAsConditional
340+
) {
341+
const innerFn = instr.value.loweredFunc;
342+
const innerTemporaries = collectTemporariesSidemap(
343+
innerFn.func,
344+
new Set(),
345+
);
346+
const optionals = collectOptionalChainSidemap(innerFn.func);
347+
const innerHoistableMap = collectHoistablePropertyLoads(
348+
innerFn.func,
349+
innerTemporaries,
350+
optionals.hoistableObjects,
351+
);
352+
const innerHoistables = assertNonNull(
353+
innerHoistableMap.get(innerFn.func.body.entry),
354+
);
355+
for (const entry of innerHoistables.assumedNonNullObjects) {
356+
assumedNonNullObjects.add(entry);
357+
}
320358
}
321359
}
322360
}
@@ -518,3 +556,25 @@ function reduceMaybeOptionalChains(
518556
}
519557
} while (changed);
520558
}
559+
560+
function collectFunctionExpressionRValues(fn: HIRFunction): Set<IdentifierId> {
561+
const sources = new Map<IdentifierId, IdentifierId>();
562+
const functionExpressionReferences = new Set<IdentifierId>();
563+
564+
for (const [_, block] of fn.body.blocks) {
565+
for (const {lvalue, value} of block.instructions) {
566+
if (value.kind === 'FunctionExpression') {
567+
for (const reference of value.loweredFunc.dependencies) {
568+
let curr: IdentifierId | undefined = reference.identifier.id;
569+
while (curr != null) {
570+
functionExpressionReferences.add(curr);
571+
curr = sources.get(curr);
572+
}
573+
}
574+
} else if (value.kind === 'PropertyLoad') {
575+
sources.set(lvalue.identifier.id, value.object.identifier.id);
576+
}
577+
}
578+
}
579+
return functionExpressionReferences;
580+
}

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
areEqualPaths,
1818
IdentifierId,
1919
} from './HIR';
20-
import {collectHoistablePropertyLoads} from './CollectHoistablePropertyLoads';
20+
import {
21+
collectHoistablePropertyLoads,
22+
keyByScopeId,
23+
} from './CollectHoistablePropertyLoads';
2124
import {
2225
ScopeBlockTraversal,
2326
eachInstructionOperand,
@@ -41,10 +44,9 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
4144
hoistableObjects,
4245
} = collectOptionalChainSidemap(fn);
4346

44-
const hoistablePropertyLoads = collectHoistablePropertyLoads(
47+
const hoistablePropertyLoads = keyByScopeId(
4548
fn,
46-
temporaries,
47-
hoistableObjects,
49+
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects),
4850
);
4951

5052
const scopeDeps = collectDependencies(
@@ -209,7 +211,7 @@ function findTemporariesUsedOutsideDeclaringScope(
209211
* of $1, as the evaluation of `arr.length` changes between instructions $1 and
210212
* $3. We do not track $1 -> arr.length in this case.
211213
*/
212-
function collectTemporariesSidemap(
214+
export function collectTemporariesSidemap(
213215
fn: HIRFunction,
214216
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
215217
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePropagateDepsInHIR
6+
7+
import { Stringify } from "shared-runtime";
8+
9+
function Foo({a, shouldReadA}) {
10+
return <Stringify fn={() => {
11+
if (shouldReadA) return a.b.c;
12+
return null;
13+
}} shouldInvokeFns={true} />
14+
}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: Foo,
18+
params: [{a: null, shouldReadA: true}],
19+
sequentialRenders: [
20+
{a: null, shouldReadA: true},
21+
{a: null, shouldReadA: false},
22+
{a: {b: {c: 4}}, shouldReadA: true},
23+
],
24+
};
25+
26+
```
27+
28+
## Code
29+
30+
```javascript
31+
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
32+
33+
import { Stringify } from "shared-runtime";
34+
35+
function Foo(t0) {
36+
const $ = _c(3);
37+
const { a, shouldReadA } = t0;
38+
let t1;
39+
if ($[0] !== shouldReadA || $[1] !== a) {
40+
t1 = (
41+
<Stringify
42+
fn={() => {
43+
if (shouldReadA) {
44+
return a.b.c;
45+
}
46+
return null;
47+
}}
48+
shouldInvokeFns={true}
49+
/>
50+
);
51+
$[0] = shouldReadA;
52+
$[1] = a;
53+
$[2] = t1;
54+
} else {
55+
t1 = $[2];
56+
}
57+
return t1;
58+
}
59+
60+
export const FIXTURE_ENTRYPOINT = {
61+
fn: Foo,
62+
params: [{ a: null, shouldReadA: true }],
63+
sequentialRenders: [
64+
{ a: null, shouldReadA: true },
65+
{ a: null, shouldReadA: false },
66+
{ a: { b: { c: 4 } }, shouldReadA: true },
67+
],
68+
};
69+
70+
```
71+
72+
### Eval output
73+
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
74+
<div>{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}</div>
75+
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @enablePropagateDepsInHIR
2+
3+
import { Stringify } from "shared-runtime";
4+
5+
function Foo({a, shouldReadA}) {
6+
return <Stringify fn={() => {
7+
if (shouldReadA) return a.b.c;
8+
return null;
9+
}} shouldInvokeFns={true} />
10+
}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: Foo,
14+
params: [{a: null, shouldReadA: true}],
15+
sequentialRenders: [
16+
{a: null, shouldReadA: true},
17+
{a: null, shouldReadA: false},
18+
{a: {b: {c: 4}}, shouldReadA: true},
19+
],
20+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePropagateDepsInHIR
6+
7+
import { Stringify } from "shared-runtime";
8+
9+
function useFoo(a) {
10+
return <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: useFoo,
15+
params: [{a: null}],
16+
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
17+
};
18+
19+
```
20+
21+
## Code
22+
23+
```javascript
24+
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
25+
26+
import { Stringify } from "shared-runtime";
27+
28+
function useFoo(a) {
29+
const $ = _c(2);
30+
let t0;
31+
if ($[0] !== a.b.c) {
32+
t0 = <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
33+
$[0] = a.b.c;
34+
$[1] = t0;
35+
} else {
36+
t0 = $[1];
37+
}
38+
return t0;
39+
}
40+
41+
export const FIXTURE_ENTRYPOINT = {
42+
fn: useFoo,
43+
params: [{ a: null }],
44+
sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }],
45+
};
46+
47+
```
48+
49+
### Eval output
50+
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
51+
[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// @enablePropagateDepsInHIR
2+
3+
import { Stringify } from "shared-runtime";
4+
5+
function useFoo(a) {
6+
return <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
7+
}
8+
9+
export const FIXTURE_ENTRYPOINT = {
10+
fn: useFoo,
11+
params: [{a: null}],
12+
sequentialRenders: [{a: null}, {a: {b: {c: 4}}}],
13+
};

0 commit comments

Comments
 (0)