Skip to content

Commit e0701a3

Browse files
committed
Force push update to [compiler][hir] Only hoist always-accessed PropertyLoads from function decls
1 parent edacbde commit e0701a3

27 files changed

+1306
-75
lines changed

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

Lines changed: 161 additions & 70 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,12 @@ import {
1516
HIRFunction,
1617
Identifier,
1718
IdentifierId,
19+
InstructionId,
1820
InstructionValue,
1921
ReactiveScopeDependency,
2022
ScopeId,
2123
} from './HIR';
24+
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
2225

2326
/**
2427
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
@@ -83,28 +86,57 @@ export function collectHoistablePropertyLoads(
8386
fn: HIRFunction,
8487
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
8588
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
86-
): ReadonlyMap<ScopeId, BlockInfo> {
89+
nestedFnImmutableContext: ReadonlySet<IdentifierId> | null,
90+
): ReadonlyMap<BlockId, BlockInfo> {
8791
const registry = new PropertyPathRegistry();
8892

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

97-
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
123+
return nodes;
124+
}
125+
126+
export function keyByScopeId<T>(
127+
fn: HIRFunction,
128+
source: ReadonlyMap<BlockId, T>,
129+
): ReadonlyMap<ScopeId, T> {
130+
const keyedByScopeId = new Map<ScopeId, T>();
98131
for (const [_, block] of fn.body.blocks) {
99132
if (block.terminal.kind === 'scope') {
100-
nodesKeyedByScopeId.set(
133+
keyedByScopeId.set(
101134
block.terminal.scope.id,
102-
nodes.get(block.terminal.block)!,
135+
source.get(block.terminal.block)!,
103136
);
104137
}
105138
}
106-
107-
return nodesKeyedByScopeId;
139+
return keyedByScopeId;
108140
}
109141

110142
export type BlockInfo = {
@@ -211,45 +243,75 @@ class PropertyPathRegistry {
211243

212244
function getMaybeNonNullInInstruction(
213245
instr: InstructionValue,
214-
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
215-
registry: PropertyPathRegistry,
246+
context: CollectNonNullsInBlocksContext,
216247
): PropertyPathNode | null {
217248
let path = null;
218249
if (instr.kind === 'PropertyLoad') {
219-
path = temporaries.get(instr.object.identifier.id) ?? {
250+
path = context.temporaries.get(instr.object.identifier.id) ?? {
220251
identifier: instr.object.identifier,
221252
path: [],
222253
};
223254
} else if (instr.kind === 'Destructure') {
224-
path = temporaries.get(instr.value.identifier.id) ?? null;
255+
path = context.temporaries.get(instr.value.identifier.id) ?? null;
225256
} else if (instr.kind === 'ComputedLoad') {
226-
path = temporaries.get(instr.object.identifier.id) ?? null;
257+
path = context.temporaries.get(instr.object.identifier.id) ?? null;
258+
}
259+
return path != null ? context.registry.getOrCreateProperty(path) : null;
260+
}
261+
262+
function isImmutableAtInstr(
263+
identifier: Identifier,
264+
instr: InstructionId,
265+
context: CollectNonNullsInBlocksContext,
266+
): boolean {
267+
if (context.nestedFnImmutableContext != null) {
268+
/**
269+
* Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered
270+
*/
271+
return context.nestedFnImmutableContext.has(identifier.id);
272+
} else {
273+
/**
274+
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
275+
* are not valid with respect to current instruction id numbering.
276+
* We use attached reactive scope ranges as a proxy for mutable range, but this
277+
* is an overestimate as (1) scope ranges merge and align to form valid program
278+
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
279+
* non-mutable identifiers.
280+
*
281+
* See comment in exported function for why we track known immutable identifiers.
282+
*/
283+
const mutableAtInstr =
284+
identifier.mutableRange.end > identifier.mutableRange.start + 1 &&
285+
identifier.scope != null &&
286+
inRange(
287+
{
288+
id: instr,
289+
},
290+
identifier.scope.range,
291+
);
292+
return (
293+
!mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id)
294+
);
227295
}
228-
return path != null ? registry.getOrCreateProperty(path) : null;
229296
}
230297

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+
};
231311
function collectNonNullsInBlocks(
232312
fn: HIRFunction,
233-
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
234-
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
235-
registry: PropertyPathRegistry,
313+
context: CollectNonNullsInBlocksContext,
236314
): ReadonlyMap<BlockId, BlockInfo> {
237-
/**
238-
* Due to current limitations of mutable range inference, there are edge cases in
239-
* which we infer known-immutable values (e.g. props or hook params) to have a
240-
* mutable range and scope.
241-
* (see `destructure-array-declaration-to-context-var` fixture)
242-
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
243-
* is being rewritten to HIR).
244-
*/
245-
const knownImmutableIdentifiers = new Set<IdentifierId>();
246-
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
247-
for (const p of fn.params) {
248-
if (p.kind === 'Identifier') {
249-
knownImmutableIdentifiers.add(p.identifier.id);
250-
}
251-
}
252-
}
253315
/**
254316
* Known non-null objects such as functional component props can be safely
255317
* read from any block.
@@ -261,53 +323,58 @@ function collectNonNullsInBlocks(
261323
fn.params[0].kind === 'Identifier'
262324
) {
263325
const identifier = fn.params[0].identifier;
264-
knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
326+
knownNonNullIdentifiers.add(
327+
context.registry.getOrCreateIdentifier(identifier),
328+
);
265329
}
266330
const nodes = new Map<BlockId, BlockInfo>();
267331
for (const [_, block] of fn.body.blocks) {
268332
const assumedNonNullObjects = new Set<PropertyPathNode>(
269333
knownNonNullIdentifiers,
270334
);
271335

272-
const maybeOptionalChain = hoistableFromOptionals.get(block.id);
336+
const maybeOptionalChain = context.hoistableFromOptionals.get(block.id);
273337
if (maybeOptionalChain != null) {
274338
assumedNonNullObjects.add(
275-
registry.getOrCreateProperty(maybeOptionalChain),
339+
context.registry.getOrCreateProperty(maybeOptionalChain),
276340
);
277341
}
278342
for (const instr of block.instructions) {
279-
const maybeNonNull = getMaybeNonNullInInstruction(
280-
instr.value,
281-
temporaries,
282-
registry,
283-
);
284-
if (maybeNonNull != null) {
285-
const baseIdentifier = maybeNonNull.fullPath.identifier;
286-
/**
287-
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
288-
* are not valid with respect to current instruction id numbering.
289-
* We use attached reactive scope ranges as a proxy for mutable range, but this
290-
* is an overestimate as (1) scope ranges merge and align to form valid program
291-
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
292-
* non-mutable identifiers.
293-
*
294-
* See comment at top of function for why we track known immutable identifiers.
295-
*/
296-
const isMutableAtInstr =
297-
baseIdentifier.mutableRange.end >
298-
baseIdentifier.mutableRange.start + 1 &&
299-
baseIdentifier.scope != null &&
300-
inRange(
301-
{
302-
id: instr.id,
303-
},
304-
baseIdentifier.scope.range,
305-
);
306-
if (
307-
!isMutableAtInstr ||
308-
knownImmutableIdentifiers.has(baseIdentifier.id)
309-
) {
310-
assumedNonNullObjects.add(maybeNonNull);
343+
const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context);
344+
if (
345+
maybeNonNull != null &&
346+
isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context)
347+
) {
348+
assumedNonNullObjects.add(maybeNonNull);
349+
}
350+
if (
351+
instr.value.kind === 'FunctionExpression' &&
352+
!fn.env.config.enableTreatFunctionDepsAsConditional
353+
) {
354+
const innerFn = instr.value.loweredFunc;
355+
const innerTemporaries = collectTemporariesSidemap(
356+
innerFn.func,
357+
new Set(),
358+
);
359+
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
360+
const innerHoistableMap = collectHoistablePropertyLoads(
361+
innerFn.func,
362+
innerTemporaries,
363+
innerOptionals.hoistableObjects,
364+
context.nestedFnImmutableContext ??
365+
new Set(
366+
innerFn.func.context
367+
.filter(place =>
368+
isImmutableAtInstr(place.identifier, instr.id, context),
369+
)
370+
.map(place => place.identifier.id),
371+
),
372+
);
373+
const innerHoistables = assertNonNull(
374+
innerHoistableMap.get(innerFn.func.body.entry),
375+
);
376+
for (const entry of innerHoistables.assumedNonNullObjects) {
377+
assumedNonNullObjects.add(entry);
311378
}
312379
}
313380
}
@@ -515,3 +582,27 @@ function reduceMaybeOptionalChains(
515582
}
516583
} while (changed);
517584
}
585+
586+
function collectFunctionExpressionFakeLoads(
587+
fn: HIRFunction,
588+
): Set<IdentifierId> {
589+
const sources = new Map<IdentifierId, IdentifierId>();
590+
const functionExpressionReferences = new Set<IdentifierId>();
591+
592+
for (const [_, block] of fn.body.blocks) {
593+
for (const {lvalue, value} of block.instructions) {
594+
if (value.kind === 'FunctionExpression') {
595+
for (const reference of value.loweredFunc.dependencies) {
596+
let curr: IdentifierId | undefined = reference.identifier.id;
597+
while (curr != null) {
598+
functionExpressionReferences.add(curr);
599+
curr = sources.get(curr);
600+
}
601+
}
602+
} else if (value.kind === 'PropertyLoad') {
603+
sources.set(lvalue.identifier.id, value.object.identifier.id);
604+
}
605+
}
606+
}
607+
return functionExpressionReferences;
608+
}

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)