Skip to content

Commit c3570b1

Browse files
authored
[compiler] Collect temporaries and optional chains from inner functions (#31346)
Recursively collect identifier / property loads and optional chains from inner functions. This PR is in preparation for #31200 Previously, we only did this in `collectHoistablePropertyLoads` to understand hoistable property loads from inner functions. 1. collectTemporariesSidemap 2. collectOptionalChainSidemap 3. collectHoistablePropertyLoads - ^ this recursively calls `collectTemporariesSidemap`, `collectOptionalChainSidemap`, and `collectOptionalChainSidemap` on inner functions 4. collectDependencies Now, we have 1. collectTemporariesSidemap - recursively record identifiers in inner functions. Note that we track all temporaries in the same map as `IdentifierIds` are currently unique across functions 2. collectOptionalChainSidemap - recursively records optional chain sidemaps in inner functions 3. collectHoistablePropertyLoads - (unchanged, except to remove recursive collection of temporaries) 4. collectDependencies - unchanged: to be modified to recursively collect dependencies in next PR ' --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31346). * #31202 * #31203 * #31201 * #31200 * __->__ #31346 * #31199
1 parent fd018af commit c3570b1

File tree

4 files changed

+139
-54
lines changed

4 files changed

+139
-54
lines changed

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Set_union,
99
getOrInsertDefault,
1010
} from '../Utils/utils';
11-
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
1211
import {
1312
BasicBlock,
1413
BlockId,
@@ -22,7 +21,6 @@ import {
2221
ReactiveScopeDependency,
2322
ScopeId,
2423
} from './HIR';
25-
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';
2624

2725
const DEBUG_PRINT = false;
2826

@@ -373,17 +371,10 @@ function collectNonNullsInBlocks(
373371
!fn.env.config.enableTreatFunctionDepsAsConditional
374372
) {
375373
const innerFn = instr.value.loweredFunc;
376-
const innerTemporaries = collectTemporariesSidemap(
377-
innerFn.func,
378-
new Set(),
379-
);
380-
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
381374
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
382375
innerFn.func,
383376
{
384377
...context,
385-
temporaries: innerTemporaries, // TODO: remove in later PR
386-
hoistableFromOptionals: innerOptionals.hoistableObjects, // TODO: remove in later PR
387378
nestedFnImmutableContext:
388379
context.nestedFnImmutableContext ??
389380
new Set(

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {assertNonNull} from './CollectHoistablePropertyLoads';
33
import {
44
BlockId,
55
BasicBlock,
6-
InstructionId,
76
IdentifierId,
87
ReactiveScopeDependency,
98
BranchTerminal,
@@ -15,32 +14,23 @@ import {
1514
OptionalTerminal,
1615
HIRFunction,
1716
DependencyPathEntry,
17+
Instruction,
18+
Terminal,
1819
} from './HIR';
1920
import {printIdentifier} from './PrintHIR';
2021

2122
export function collectOptionalChainSidemap(
2223
fn: HIRFunction,
2324
): OptionalChainSidemap {
2425
const context: OptionalTraversalContext = {
26+
currFn: fn,
2527
blocks: fn.body.blocks,
2628
seenOptionals: new Set(),
2729
processedInstrsInOptional: new Set(),
2830
temporariesReadInOptional: new Map(),
2931
hoistableObjects: new Map(),
3032
};
31-
for (const [_, block] of fn.body.blocks) {
32-
if (
33-
block.terminal.kind === 'optional' &&
34-
!context.seenOptionals.has(block.id)
35-
) {
36-
traverseOptionalBlock(
37-
block as TBasicBlock<OptionalTerminal>,
38-
context,
39-
null,
40-
);
41-
}
42-
}
43-
33+
traverseFunction(fn, context);
4434
return {
4535
temporariesReadInOptional: context.temporariesReadInOptional,
4636
processedInstrsInOptional: context.processedInstrsInOptional,
@@ -96,8 +86,10 @@ export type OptionalChainSidemap = {
9686
* bb5:
9787
* $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
9888
* ```
89+
*
90+
* Also note that InstructionIds are not unique across inner functions.
9991
*/
100-
processedInstrsInOptional: ReadonlySet<InstructionId>;
92+
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
10193
/**
10294
* Records optional chains for which we can safely evaluate non-optional
10395
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
@@ -115,16 +107,46 @@ export type OptionalChainSidemap = {
115107
};
116108

117109
type OptionalTraversalContext = {
110+
currFn: HIRFunction;
118111
blocks: ReadonlyMap<BlockId, BasicBlock>;
119112

120113
// Track optional blocks to avoid outer calls into nested optionals
121114
seenOptionals: Set<BlockId>;
122115

123-
processedInstrsInOptional: Set<InstructionId>;
116+
processedInstrsInOptional: Set<Instruction | Terminal>;
124117
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
125118
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
126119
};
127120

121+
function traverseFunction(
122+
fn: HIRFunction,
123+
context: OptionalTraversalContext,
124+
): void {
125+
for (const [_, block] of fn.body.blocks) {
126+
for (const instr of block.instructions) {
127+
if (
128+
instr.value.kind === 'FunctionExpression' ||
129+
instr.value.kind === 'ObjectMethod'
130+
) {
131+
traverseFunction(instr.value.loweredFunc.func, {
132+
...context,
133+
currFn: instr.value.loweredFunc.func,
134+
blocks: instr.value.loweredFunc.func.body.blocks,
135+
});
136+
}
137+
}
138+
if (
139+
block.terminal.kind === 'optional' &&
140+
!context.seenOptionals.has(block.id)
141+
) {
142+
traverseOptionalBlock(
143+
block as TBasicBlock<OptionalTerminal>,
144+
context,
145+
null,
146+
);
147+
}
148+
}
149+
}
128150
/**
129151
* Match the consequent and alternate blocks of an optional.
130152
* @returns propertyload computed by the consequent block, or null if the
@@ -137,7 +159,7 @@ function matchOptionalTestBlock(
137159
consequentId: IdentifierId;
138160
property: string;
139161
propertyId: IdentifierId;
140-
storeLocalInstrId: InstructionId;
162+
storeLocalInstr: Instruction;
141163
consequentGoto: BlockId;
142164
} | null {
143165
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
@@ -149,7 +171,7 @@ function matchOptionalTestBlock(
149171
const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
150172
.instructions[0] as TInstruction<PropertyLoad>;
151173
const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
152-
const storeLocalInstrId = consequentBlock.instructions[1].id;
174+
const storeLocalInstr = consequentBlock.instructions[1];
153175
CompilerError.invariant(
154176
propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
155177
{
@@ -189,7 +211,7 @@ function matchOptionalTestBlock(
189211
consequentId: storeLocal.lvalue.place.identifier.id,
190212
property: propertyLoad.value.property,
191213
propertyId: propertyLoad.lvalue.identifier.id,
192-
storeLocalInstrId,
214+
storeLocalInstr,
193215
consequentGoto: consequentBlock.terminal.block,
194216
};
195217
}
@@ -369,10 +391,8 @@ function traverseOptionalBlock(
369391
},
370392
],
371393
};
372-
context.processedInstrsInOptional.add(
373-
matchConsequentResult.storeLocalInstrId,
374-
);
375-
context.processedInstrsInOptional.add(test.id);
394+
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
395+
context.processedInstrsInOptional.add(test);
376396
context.temporariesReadInOptional.set(
377397
matchConsequentResult.consequentId,
378398
load,

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

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
DeclarationId,
1717
areEqualPaths,
1818
IdentifierId,
19+
Terminal,
1920
} from './HIR';
2021
import {
2122
collectHoistablePropertyLoads,
@@ -176,8 +177,10 @@ function findTemporariesUsedOutsideDeclaringScope(
176177
* $2 = LoadLocal 'foo'
177178
* $3 = CallExpression $2($1)
178179
* ```
179-
* Only map LoadLocal and PropertyLoad lvalues to their source if we know that
180-
* reordering the read (from the time-of-load to time-of-use) is valid.
180+
* @param usedOutsideDeclaringScope is used to check the correctness of
181+
* reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
182+
* PropertyLoad in the returned temporaries map if reordering the read (from the
183+
* time-of-load to time-of-use) is valid.
181184
*
182185
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
183186
* (a proxy for mutable range) of the load source, later instructions may
@@ -215,7 +218,29 @@ export function collectTemporariesSidemap(
215218
fn: HIRFunction,
216219
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
217220
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
218-
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
221+
const temporaries = new Map();
222+
collectTemporariesSidemapImpl(
223+
fn,
224+
usedOutsideDeclaringScope,
225+
temporaries,
226+
false,
227+
);
228+
return temporaries;
229+
}
230+
231+
/**
232+
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
233+
* function and all nested functions.
234+
*
235+
* Note that IdentifierIds are currently unique, so we can use a single
236+
* Map<IdentifierId, ...> across all nested functions.
237+
*/
238+
function collectTemporariesSidemapImpl(
239+
fn: HIRFunction,
240+
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
241+
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
242+
isInnerFn: boolean,
243+
): void {
219244
for (const [_, block] of fn.body.blocks) {
220245
for (const instr of block.instructions) {
221246
const {value, lvalue} = instr;
@@ -224,27 +249,51 @@ export function collectTemporariesSidemap(
224249
);
225250

226251
if (value.kind === 'PropertyLoad' && !usedOutside) {
227-
const property = getProperty(
228-
value.object,
229-
value.property,
230-
false,
231-
temporaries,
232-
);
233-
temporaries.set(lvalue.identifier.id, property);
252+
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
253+
/**
254+
* All dependencies of a inner / nested function must have a base
255+
* identifier from the outermost component / hook. This is because the
256+
* compiler cannot break an inner function into multiple granular
257+
* scopes.
258+
*/
259+
const property = getProperty(
260+
value.object,
261+
value.property,
262+
false,
263+
temporaries,
264+
);
265+
temporaries.set(lvalue.identifier.id, property);
266+
}
234267
} else if (
235268
value.kind === 'LoadLocal' &&
236269
lvalue.identifier.name == null &&
237270
value.place.identifier.name !== null &&
238271
!usedOutside
239272
) {
240-
temporaries.set(lvalue.identifier.id, {
241-
identifier: value.place.identifier,
242-
path: [],
243-
});
273+
if (
274+
!isInnerFn ||
275+
fn.context.some(
276+
context => context.identifier.id === value.place.identifier.id,
277+
)
278+
) {
279+
temporaries.set(lvalue.identifier.id, {
280+
identifier: value.place.identifier,
281+
path: [],
282+
});
283+
}
284+
} else if (
285+
value.kind === 'FunctionExpression' ||
286+
value.kind === 'ObjectMethod'
287+
) {
288+
collectTemporariesSidemapImpl(
289+
value.loweredFunc.func,
290+
usedOutsideDeclaringScope,
291+
temporaries,
292+
true,
293+
);
244294
}
245295
}
246296
}
247-
return temporaries;
248297
}
249298

250299
function getProperty(
@@ -310,6 +359,12 @@ class Context {
310359
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
311360
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
312361

362+
/**
363+
* Tracks the traversal state. See Context.declare for explanation of why this
364+
* is needed.
365+
*/
366+
inInnerFn: boolean = false;
367+
313368
constructor(
314369
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
315370
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
@@ -360,12 +415,23 @@ class Context {
360415
}
361416

362417
/*
363-
* Records where a value was declared, and optionally, the scope where the value originated from.
364-
* This is later used to determine if a dependency should be added to a scope; if the current
365-
* scope we are visiting is the same scope where the value originates, it can't be a dependency
366-
* on itself.
418+
* Records where a value was declared, and optionally, the scope where the
419+
* value originated from. This is later used to determine if a dependency
420+
* should be added to a scope; if the current scope we are visiting is the
421+
* same scope where the value originates, it can't be a dependency on itself.
422+
*
423+
* Note that we do not track declarations or reassignments within inner
424+
* functions for the following reasons:
425+
* - inner functions cannot be split by scope boundaries and are guaranteed
426+
* to consume their own declarations
427+
* - reassignments within inner functions are tracked as context variables,
428+
* which already have extended mutable ranges to account for reassignments
429+
* - *most importantly* it's currently simply incorrect to compare inner
430+
* function instruction ids (tracked by `decl`) with outer ones (as stored
431+
* by root identifier mutable ranges).
367432
*/
368433
declare(identifier: Identifier, decl: Decl): void {
434+
if (this.inInnerFn) return;
369435
if (!this.#declarations.has(identifier.declarationId)) {
370436
this.#declarations.set(identifier.declarationId, decl);
371437
}
@@ -575,7 +641,7 @@ function collectDependencies(
575641
fn: HIRFunction,
576642
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
577643
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
578-
processedInstrsInOptional: ReadonlySet<InstructionId>,
644+
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
579645
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
580646
const context = new Context(usedOutsideDeclaringScope, temporaries);
581647

@@ -614,12 +680,12 @@ function collectDependencies(
614680
}
615681
}
616682
for (const instr of block.instructions) {
617-
if (!processedInstrsInOptional.has(instr.id)) {
683+
if (!processedInstrsInOptional.has(instr)) {
618684
handleInstruction(instr, context);
619685
}
620686
}
621687

622-
if (!processedInstrsInOptional.has(block.terminal.id)) {
688+
if (!processedInstrsInOptional.has(block.terminal)) {
623689
for (const place of eachTerminalOperand(block.terminal)) {
624690
context.visitOperand(place);
625691
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,9 +1215,17 @@ export class ScopeBlockTraversal {
12151215
}
12161216
}
12171217

1218+
/**
1219+
* @returns if the given scope is currently 'active', i.e. if the scope start
1220+
* block but not the scope fallthrough has been recorded.
1221+
*/
12181222
isScopeActive(scopeId: ScopeId): boolean {
12191223
return this.#activeScopes.indexOf(scopeId) !== -1;
12201224
}
1225+
1226+
/**
1227+
* The current, innermost active scope.
1228+
*/
12211229
get currentScope(): ScopeId | null {
12221230
return this.#activeScopes.at(-1) ?? null;
12231231
}

0 commit comments

Comments
 (0)