Skip to content

Commit ab8d3c5

Browse files
committed
[compiler] bugfix for hoistable deps for nested functions
`PropertyPathRegistry` is responsible for uniqueing identifier and property paths. This is necessary for the hoistability CFG merging logic which takes unions and intersections of these nodes to determine a basic block's hoistable reads, as a function of its neighbors. We also depend on this to merge optional chained and non-optional chained property paths This fixes a small bug in #31066 in which we create a new registry for nested functions. Now, we use the same registry for a component / hook and all its inner functions '
1 parent 1c6a90e commit ab8d3c5

File tree

4 files changed

+138
-39
lines changed

4 files changed

+138
-39
lines changed

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

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {CompilerError} from '../CompilerError';
22
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
3+
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
34
import {
45
Set_equal,
56
Set_filter,
@@ -23,6 +24,8 @@ import {
2324
} from './HIR';
2425
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
2526

27+
const DEBUG_PRINT = false;
28+
2629
/**
2730
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
2831
* analysis to determine which `Identifier`s can be assumed to be non-null
@@ -86,15 +89,8 @@ export function collectHoistablePropertyLoads(
8689
fn: HIRFunction,
8790
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
8891
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
89-
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null,
9092
): ReadonlyMap<BlockId, BlockInfo> {
9193
const registry = new PropertyPathRegistry();
92-
93-
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
94-
const actuallyEvaluatedTemporaries = new Map(
95-
[...temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
96-
);
97-
9894
/**
9995
* Due to current limitations of mutable range inference, there are edge cases in
10096
* which we infer known-immutable values (e.g. props or hook params) to have a
@@ -111,14 +107,51 @@ export function collectHoistablePropertyLoads(
111107
}
112108
}
113109
}
114-
const nodes = collectNonNullsInBlocks(fn, {
115-
temporaries: actuallyEvaluatedTemporaries,
110+
return collectHoistablePropertyLoadsImpl(fn, {
111+
temporaries,
116112
knownImmutableIdentifiers,
117113
hoistableFromOptionals,
118114
registry,
119-
nestedFnImmutableContext,
115+
nestedFnImmutableContext: null,
120116
});
121-
propagateNonNull(fn, nodes, registry);
117+
}
118+
119+
type CollectHoistablePropertyLoadsContext = {
120+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
121+
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
122+
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
123+
registry: PropertyPathRegistry;
124+
/**
125+
* (For nested / inner function declarations)
126+
* Context variables (i.e. captured from an outer scope) that are immutable.
127+
* Note that this technically could be merged into `knownImmutableIdentifiers`,
128+
* but are currently kept separate for readability.
129+
*/
130+
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
131+
};
132+
function collectHoistablePropertyLoadsImpl(
133+
fn: HIRFunction,
134+
context: CollectHoistablePropertyLoadsContext,
135+
): ReadonlyMap<BlockId, BlockInfo> {
136+
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
137+
const actuallyEvaluatedTemporaries = new Map(
138+
[...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
139+
);
140+
141+
const nodes = collectNonNullsInBlocks(fn, {
142+
...context,
143+
temporaries: actuallyEvaluatedTemporaries,
144+
});
145+
propagateNonNull(fn, nodes, context.registry);
146+
147+
if (DEBUG_PRINT) {
148+
console.log('(printing hoistable nodes in blocks)');
149+
for (const [blockId, node] of nodes) {
150+
console.log(
151+
`bb${blockId}: ${[...node.assumedNonNullObjects].map(n => printDependency(n.fullPath)).join(' ')}`,
152+
);
153+
}
154+
}
122155

123156
return nodes;
124157
}
@@ -243,7 +276,7 @@ class PropertyPathRegistry {
243276

244277
function getMaybeNonNullInInstruction(
245278
instr: InstructionValue,
246-
context: CollectNonNullsInBlocksContext,
279+
context: CollectHoistablePropertyLoadsContext,
247280
): PropertyPathNode | null {
248281
let path = null;
249282
if (instr.kind === 'PropertyLoad') {
@@ -262,7 +295,7 @@ function getMaybeNonNullInInstruction(
262295
function isImmutableAtInstr(
263296
identifier: Identifier,
264297
instr: InstructionId,
265-
context: CollectNonNullsInBlocksContext,
298+
context: CollectHoistablePropertyLoadsContext,
266299
): boolean {
267300
if (context.nestedFnImmutableContext != null) {
268301
/**
@@ -295,22 +328,9 @@ function isImmutableAtInstr(
295328
}
296329
}
297330

298-
type CollectNonNullsInBlocksContext = {
299-
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
300-
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
301-
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
302-
registry: PropertyPathRegistry;
303-
/**
304-
* (For nested / inner function declarations)
305-
* Context variables (i.e. captured from an outer scope) that are immutable.
306-
* Note that this technically could be merged into `knownImmutableIdentifiers`,
307-
* but are currently kept separate for readability.
308-
*/
309-
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null;
310-
};
311331
function collectNonNullsInBlocks(
312332
fn: HIRFunction,
313-
context: CollectNonNullsInBlocksContext,
333+
context: CollectHoistablePropertyLoadsContext,
314334
): ReadonlyMap<BlockId, BlockInfo> {
315335
/**
316336
* Known non-null objects such as functional component props can be safely
@@ -358,18 +378,22 @@ function collectNonNullsInBlocks(
358378
new Set(),
359379
);
360380
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
361-
const innerHoistableMap = collectHoistablePropertyLoads(
381+
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
362382
innerFn.func,
363-
innerTemporaries,
364-
innerOptionals.hoistableObjects,
365-
context.nestedFnImmutableContext ??
366-
new Set(
367-
innerFn.func.context
368-
.filter(place =>
369-
isImmutableAtInstr(place.identifier, instr.id, context),
370-
)
371-
.map(place => place.identifier.id),
372-
),
383+
{
384+
...context,
385+
temporaries: innerTemporaries, // TODO: remove in later PR
386+
hoistableFromOptionals: innerOptionals.hoistableObjects, // TODO: remove in later PR
387+
nestedFnImmutableContext:
388+
context.nestedFnImmutableContext ??
389+
new Set(
390+
innerFn.func.context
391+
.filter(place =>
392+
isImmutableAtInstr(place.identifier, instr.id, context),
393+
)
394+
.map(place => place.identifier.id),
395+
),
396+
},
373397
);
374398
const innerHoistables = assertNonNull(
375399
innerHoistableMap.get(innerFn.func.body.entry),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
4646

4747
const hoistablePropertyLoads = keyByScopeId(
4848
fn,
49-
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects, null),
49+
collectHoistablePropertyLoads(fn, temporaries, hoistableObjects),
5050
);
5151

5252
const scopeDeps = collectDependencies(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enablePropagateDepsInHIR
6+
import {Stringify} from 'shared-runtime';
7+
8+
function Foo({data}) {
9+
return (
10+
<Stringify foo={() => data.a.d} bar={data.a?.b.c} shouldInvokeFns={true} />
11+
);
12+
}
13+
14+
export const FIXTURE_ENTRYPOINT = {
15+
fn: Foo,
16+
params: [{data: {a: null}}],
17+
sequentialRenders: [{data: {a: {b: {c: 4}}}}],
18+
};
19+
20+
```
21+
22+
## Code
23+
24+
```javascript
25+
import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR
26+
import { Stringify } from "shared-runtime";
27+
28+
function Foo(t0) {
29+
const $ = _c(5);
30+
const { data } = t0;
31+
let t1;
32+
if ($[0] !== data.a.d) {
33+
t1 = () => data.a.d;
34+
$[0] = data.a.d;
35+
$[1] = t1;
36+
} else {
37+
t1 = $[1];
38+
}
39+
const t2 = data.a?.b.c;
40+
let t3;
41+
if ($[2] !== t1 || $[3] !== t2) {
42+
t3 = <Stringify foo={t1} bar={t2} shouldInvokeFns={true} />;
43+
$[2] = t1;
44+
$[3] = t2;
45+
$[4] = t3;
46+
} else {
47+
t3 = $[4];
48+
}
49+
return t3;
50+
}
51+
52+
export const FIXTURE_ENTRYPOINT = {
53+
fn: Foo,
54+
params: [{ data: { a: null } }],
55+
sequentialRenders: [{ data: { a: { b: { c: 4 } } } }],
56+
};
57+
58+
```
59+
60+
### Eval output
61+
(kind: ok) <div>{"foo":{"kind":"Function"},"bar":4,"shouldInvokeFns":true}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @enablePropagateDepsInHIR
2+
import {Stringify} from 'shared-runtime';
3+
4+
function Foo({data}) {
5+
return (
6+
<Stringify foo={() => data.a.d} bar={data.a?.b.c} shouldInvokeFns={true} />
7+
);
8+
}
9+
10+
export const FIXTURE_ENTRYPOINT = {
11+
fn: Foo,
12+
params: [{data: {a: null}}],
13+
sequentialRenders: [{data: {a: {b: {c: 4}}}}],
14+
};

0 commit comments

Comments
 (0)