Skip to content

Commit 8273cd1

Browse files
committed
[compiler][hir] Only hoist always-accessed PropertyLoads from function decls
ghstack-source-id: 485a44f Pull Request resolved: #31066
1 parent 18d35c5 commit 8273cd1

40 files changed

Lines changed: 1683 additions & 378 deletions

File tree

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

Lines changed: 137 additions & 43 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,
@@ -15,10 +16,13 @@ import {
1516
HIRFunction,
1617
Identifier,
1718
IdentifierId,
19+
Instruction,
20+
InstructionId,
1821
InstructionValue,
1922
ReactiveScopeDependency,
2023
ScopeId,
2124
} from './HIR';
25+
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
2226

2327
/**
2428
* Helper function for `PropagateScopeDependencies`.
@@ -73,30 +77,73 @@ export function collectHoistablePropertyLoads(
7377
fn: HIRFunction,
7478
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
7579
optionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
76-
): ReadonlyMap<ScopeId, BlockInfo> {
80+
nestedFnContext: NestedFunctionContext | null,
81+
): ReadonlyMap<BlockId, BlockInfo> {
7782
const registry = new PropertyPathRegistry();
7883

79-
const nodes = collectNonNullsInBlocks(fn, temporaries, optionals, registry);
84+
const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn);
85+
const actuallyEvaluatedTemporaries = new Map(
86+
[...temporaries].filter(([id]) => !functionExpressionLoads.has(id)),
87+
);
88+
89+
/**
90+
* Due to current limitations of mutable range inference, there are edge cases in
91+
* which we infer known-immutable values (e.g. props or hook params) to have a
92+
* mutable range and scope.
93+
* (see `destructure-array-declaration-to-context-var` fixture)
94+
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
95+
* is being rewritten to HIR).
96+
*/
97+
const knownImmutableIdentifiers = new Set<IdentifierId>();
98+
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
99+
for (const p of fn.params) {
100+
if (p.kind === 'Identifier') {
101+
knownImmutableIdentifiers.add(p.identifier.id);
102+
}
103+
}
104+
}
105+
const nodes = collectNonNullsInBlocks(fn, {
106+
temporaries: actuallyEvaluatedTemporaries,
107+
knownImmutableIdentifiers,
108+
optionals,
109+
registry,
110+
nestedFnContext,
111+
});
80112
propagateNonNull(fn, nodes, registry);
81113

82-
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
114+
return nodes;
115+
}
116+
117+
export function keyByScopeId<T>(
118+
fn: HIRFunction,
119+
source: ReadonlyMap<BlockId, T>,
120+
): ReadonlyMap<ScopeId, T> {
121+
const keyedByScopeId = new Map<ScopeId, T>();
83122
for (const [_, block] of fn.body.blocks) {
84123
if (block.terminal.kind === 'scope') {
85-
nodesKeyedByScopeId.set(
124+
keyedByScopeId.set(
86125
block.terminal.scope.id,
87-
nodes.get(block.terminal.block)!,
126+
source.get(block.terminal.block)!,
88127
);
89128
}
90129
}
91-
92-
return nodesKeyedByScopeId;
130+
return keyedByScopeId;
93131
}
94132

95133
export type BlockInfo = {
96134
block: BasicBlock;
97135
assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
98136
};
99137

138+
type NestedFunctionContext = {
139+
/**
140+
* Instruction id of the outermost nested function declaration.
141+
*/
142+
outermostFnInstrId: InstructionId;
143+
// Captured from outermost scope
144+
capturedIdentifiers: Set<IdentifierId>;
145+
};
146+
100147
/**
101148
* PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
102149
* and make computing sets intersections simpler.
@@ -196,45 +243,33 @@ class PropertyPathRegistry {
196243

197244
function getMaybeNonNullInInstruction(
198245
instr: InstructionValue,
199-
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
200-
registry: PropertyPathRegistry,
246+
context: CollectNonNullsInBlocksContext,
201247
): PropertyPathNode | null {
202248
let path = null;
203249
if (instr.kind === 'PropertyLoad') {
204-
path = temporaries.get(instr.object.identifier.id) ?? {
250+
path = context.temporaries.get(instr.object.identifier.id) ?? {
205251
identifier: instr.object.identifier,
206252
path: [],
207253
};
208254
} else if (instr.kind === 'Destructure') {
209-
path = temporaries.get(instr.value.identifier.id) ?? null;
255+
path = context.temporaries.get(instr.value.identifier.id) ?? null;
210256
} else if (instr.kind === 'ComputedLoad') {
211-
path = temporaries.get(instr.object.identifier.id) ?? null;
257+
path = context.temporaries.get(instr.object.identifier.id) ?? null;
212258
}
213-
return path != null ? registry.getOrCreateProperty(path) : null;
259+
return path != null ? context.registry.getOrCreateProperty(path) : null;
214260
}
215261

262+
type CollectNonNullsInBlocksContext = {
263+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
264+
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
265+
optionals: ReadonlyMap<BlockId, ReactiveScopeDependency>;
266+
registry: PropertyPathRegistry;
267+
nestedFnContext: NestedFunctionContext | null;
268+
};
216269
function collectNonNullsInBlocks(
217270
fn: HIRFunction,
218-
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
219-
optionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
220-
registry: PropertyPathRegistry,
271+
context: CollectNonNullsInBlocksContext,
221272
): ReadonlyMap<BlockId, BlockInfo> {
222-
/**
223-
* Due to current limitations of mutable range inference, there are edge cases in
224-
* which we infer known-immutable values (e.g. props or hook params) to have a
225-
* mutable range and scope.
226-
* (see `destructure-array-declaration-to-context-var` fixture)
227-
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
228-
* is being rewritten to HIR).
229-
*/
230-
const knownImmutableIdentifiers = new Set<IdentifierId>();
231-
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
232-
for (const p of fn.params) {
233-
if (p.kind === 'Identifier') {
234-
knownImmutableIdentifiers.add(p.identifier.id);
235-
}
236-
}
237-
}
238273
/**
239274
* Known non-null objects such as functional component props can be safely
240275
* read from any block.
@@ -246,7 +281,9 @@ function collectNonNullsInBlocks(
246281
fn.params[0].kind === 'Identifier'
247282
) {
248283
const identifier = fn.params[0].identifier;
249-
knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
284+
knownNonNullIdentifiers.add(
285+
context.registry.getOrCreateIdentifier(identifier),
286+
);
250287
}
251288
const nodes = new Map<BlockId, BlockInfo>();
252289
for (const [_, block] of fn.body.blocks) {
@@ -258,20 +295,22 @@ function collectNonNullsInBlocks(
258295
block,
259296
assumedNonNullObjects,
260297
});
261-
const maybeOptionalChain = optionals.get(block.id);
298+
const maybeOptionalChain = context.optionals.get(block.id);
262299
if (maybeOptionalChain != null) {
263300
assumedNonNullObjects.add(
264-
registry.getOrCreateProperty(maybeOptionalChain),
301+
context.registry.getOrCreateProperty(maybeOptionalChain),
265302
);
266303
continue;
267304
}
268305
for (const instr of block.instructions) {
269-
const maybeNonNull = getMaybeNonNullInInstruction(
270-
instr.value,
271-
temporaries,
272-
registry,
273-
);
274-
if (maybeNonNull != null) {
306+
const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
307+
if (
308+
maybeNonNull != null &&
309+
(context.nestedFnContext == null ||
310+
context.nestedFnContext.capturedIdentifiers.has(
311+
maybeNonNull.fullPath.identifier.id,
312+
))
313+
) {
275314
const baseIdentifier = maybeNonNull.fullPath.identifier;
276315
/**
277316
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
@@ -289,17 +328,48 @@ function collectNonNullsInBlocks(
289328
baseIdentifier.scope != null &&
290329
inRange(
291330
{
292-
id: instr.id,
331+
id:
332+
context.nestedFnContext != null
333+
? context.nestedFnContext.outermostFnInstrId
334+
: instr.id,
293335
},
294336
baseIdentifier.scope.range,
295337
);
296338
if (
297339
!isMutableAtInstr ||
298-
knownImmutableIdentifiers.has(baseIdentifier.id)
340+
context.knownImmutableIdentifiers.has(baseIdentifier.id)
299341
) {
300342
assumedNonNullObjects.add(maybeNonNull);
301343
}
302344
}
345+
if (
346+
instr.value.kind === 'FunctionExpression' &&
347+
!fn.env.config.enableTreatFunctionDepsAsConditional
348+
) {
349+
const innerFn = instr.value.loweredFunc;
350+
const innerTemporaries = collectTemporariesSidemap(
351+
innerFn.func,
352+
new Set(),
353+
);
354+
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
355+
const innerHoistableMap = collectHoistablePropertyLoads(
356+
innerFn.func,
357+
innerTemporaries,
358+
innerOptionals.hoistableObjects,
359+
context.nestedFnContext ?? {
360+
outermostFnInstrId: instr.id,
361+
capturedIdentifiers: new Set(
362+
innerFn.func.context.map(place => place.identifier.id),
363+
),
364+
},
365+
);
366+
const innerHoistables = assertNonNull(
367+
innerHoistableMap.get(innerFn.func.body.entry),
368+
);
369+
for (const entry of innerHoistables.assumedNonNullObjects) {
370+
assumedNonNullObjects.add(entry);
371+
}
372+
}
303373
}
304374
}
305375
return nodes;
@@ -500,3 +570,27 @@ function reduceMaybeOptionalChains(
500570
}
501571
} while (changed);
502572
}
573+
574+
function collectFunctionExpressionFakeLoads(
575+
fn: HIRFunction,
576+
): Set<IdentifierId> {
577+
const sources = new Map<IdentifierId, IdentifierId>();
578+
const functionExpressionReferences = new Set<IdentifierId>();
579+
580+
for (const [_, block] of fn.body.blocks) {
581+
for (const {lvalue, value} of block.instructions) {
582+
if (value.kind === 'FunctionExpression') {
583+
for (const reference of value.loweredFunc.dependencies) {
584+
let curr: IdentifierId | undefined = reference.identifier.id;
585+
while (curr != null) {
586+
functionExpressionReferences.add(curr);
587+
curr = sources.get(curr);
588+
}
589+
}
590+
} else if (value.kind === 'PropertyLoad') {
591+
sources.set(lvalue.identifier.id, value.object.identifier.id);
592+
}
593+
}
594+
}
595+
return functionExpressionReferences;
596+
}

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, null),
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> {

0 commit comments

Comments
 (0)