Skip to content

Commit a9e003b

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

23 files changed

Lines changed: 1020 additions & 12 deletions

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 = {
@@ -319,6 +336,27 @@ function collectNonNullsInBlocks(
319336
assumedNonNullObjects,
320337
);
321338
}
339+
} else if (
340+
instr.value.kind === 'FunctionExpression' &&
341+
!fn.env.config.enableTreatFunctionDepsAsConditional
342+
) {
343+
const innerFn = instr.value.loweredFunc;
344+
const innerTemporaries = collectTemporariesSidemap(
345+
innerFn.func,
346+
new Set(),
347+
);
348+
const optionals = collectOptionalChainSidemap(innerFn.func);
349+
const innerHoistableMap = collectHoistablePropertyLoads(
350+
innerFn.func,
351+
innerTemporaries,
352+
optionals.hoistableObjects,
353+
);
354+
const innerHoistables = assertNonNull(
355+
innerHoistableMap.get(innerFn.func.body.entry),
356+
);
357+
for (const entry of innerHoistables.assumedNonNullObjects) {
358+
assumedNonNullObjects.add(entry);
359+
}
322360
}
323361
}
324362
}
@@ -520,3 +558,25 @@ function reduceMaybeOptionalChains(
520558
}
521559
} while (changed);
522560
}
561+
562+
function collectFunctionExpressionRValues(fn: HIRFunction): Set<IdentifierId> {
563+
const sources = new Map<IdentifierId, IdentifierId>();
564+
const functionExpressionReferences = new Set<IdentifierId>();
565+
566+
for (const [_, block] of fn.body.blocks) {
567+
for (const {lvalue, value} of block.instructions) {
568+
if (value.kind === 'FunctionExpression') {
569+
for (const reference of value.loweredFunc.dependencies) {
570+
let curr: IdentifierId | undefined = reference.identifier.id;
571+
while (curr != null) {
572+
functionExpressionReferences.add(curr);
573+
curr = sources.get(curr);
574+
}
575+
}
576+
} else if (value.kind === 'PropertyLoad') {
577+
sources.set(lvalue.identifier.id, value.object.identifier.id);
578+
}
579+
}
580+
}
581+
return functionExpressionReferences;
582+
}

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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePropagateDepsInHIR
6+
7+
import {Stringify} from 'shared-runtime';
8+
9+
function Foo({a, shouldReadA}) {
10+
return (
11+
<Stringify
12+
fn={() => {
13+
if (shouldReadA) return a.b.c;
14+
return null;
15+
}}
16+
shouldInvokeFns={true}
17+
/>
18+
);
19+
}
20+
21+
export const FIXTURE_ENTRYPOINT = {
22+
fn: Foo,
23+
params: [{a: null, shouldReadA: true}],
24+
sequentialRenders: [
25+
{a: null, shouldReadA: true},
26+
{a: null, shouldReadA: false},
27+
{a: {b: {c: 4}}, shouldReadA: true},
28+
],
29+
};
30+
31+
```
32+
33+
## Code
34+
35+
```javascript
36+
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
37+
38+
import { Stringify } from "shared-runtime";
39+
40+
function Foo(t0) {
41+
const $ = _c(3);
42+
const { a, shouldReadA } = t0;
43+
let t1;
44+
if ($[0] !== shouldReadA || $[1] !== a) {
45+
t1 = (
46+
<Stringify
47+
fn={() => {
48+
if (shouldReadA) {
49+
return a.b.c;
50+
}
51+
return null;
52+
}}
53+
shouldInvokeFns={true}
54+
/>
55+
);
56+
$[0] = shouldReadA;
57+
$[1] = a;
58+
$[2] = t1;
59+
} else {
60+
t1 = $[2];
61+
}
62+
return t1;
63+
}
64+
65+
export const FIXTURE_ENTRYPOINT = {
66+
fn: Foo,
67+
params: [{ a: null, shouldReadA: true }],
68+
sequentialRenders: [
69+
{ a: null, shouldReadA: true },
70+
{ a: null, shouldReadA: false },
71+
{ a: { b: { c: 4 } }, shouldReadA: true },
72+
],
73+
};
74+
75+
```
76+
77+
### Eval output
78+
(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]]
79+
<div>{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}</div>
80+
<div>{"fn":{"kind":"Function","result":4},"shouldInvokeFns":true}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @enablePropagateDepsInHIR
2+
3+
import {Stringify} from 'shared-runtime';
4+
5+
function Foo({a, shouldReadA}) {
6+
return (
7+
<Stringify
8+
fn={() => {
9+
if (shouldReadA) return a.b.c;
10+
return null;
11+
}}
12+
shouldInvokeFns={true}
13+
/>
14+
);
15+
}
16+
17+
export const FIXTURE_ENTRYPOINT = {
18+
fn: Foo,
19+
params: [{a: null, shouldReadA: true}],
20+
sequentialRenders: [
21+
{a: null, shouldReadA: true},
22+
{a: null, shouldReadA: false},
23+
{a: {b: {c: 4}}, shouldReadA: true},
24+
],
25+
};
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) {
32+
t0 = <Stringify fn={() => a.b.c} shouldInvokeFns={true} />;
33+
$[0] = a;
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)