Skip to content

Commit 0d4afc5

Browse files
committed
[compiler] Collect temporaries and optional chains from inner functions
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 '
1 parent 640c52e commit 0d4afc5

File tree

4 files changed

+151
-50
lines changed

4 files changed

+151
-50
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: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {CompilerError} from '..';
2+
import {getOrInsertDefault} from '../Utils/utils';
23
import {assertNonNull} from './CollectHoistablePropertyLoads';
34
import {
45
BlockId,
@@ -22,25 +23,14 @@ export function collectOptionalChainSidemap(
2223
fn: HIRFunction,
2324
): OptionalChainSidemap {
2425
const context: OptionalTraversalContext = {
26+
currFn: fn,
2527
blocks: fn.body.blocks,
2628
seenOptionals: new Set(),
27-
processedInstrsInOptional: new Set(),
29+
processedInstrsInOptional: new Map(),
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,13 @@ 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: ReadonlyMap<
93+
HIRFunction,
94+
ReadonlySet<InstructionId>
95+
>;
10196
/**
10297
* Records optional chains for which we can safely evaluate non-optional
10398
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
@@ -115,16 +110,46 @@ export type OptionalChainSidemap = {
115110
};
116111

117112
type OptionalTraversalContext = {
113+
currFn: HIRFunction;
118114
blocks: ReadonlyMap<BlockId, BasicBlock>;
119115

120116
// Track optional blocks to avoid outer calls into nested optionals
121117
seenOptionals: Set<BlockId>;
122118

123-
processedInstrsInOptional: Set<InstructionId>;
119+
processedInstrsInOptional: Map<HIRFunction, Set<InstructionId>>;
124120
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
125121
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
126122
};
127123

124+
function traverseFunction(
125+
fn: HIRFunction,
126+
context: OptionalTraversalContext,
127+
): void {
128+
for (const [_, block] of fn.body.blocks) {
129+
for (const instr of block.instructions) {
130+
if (
131+
instr.value.kind === 'FunctionExpression' ||
132+
instr.value.kind === 'ObjectMethod'
133+
) {
134+
traverseFunction(instr.value.loweredFunc.func, {
135+
...context,
136+
currFn: instr.value.loweredFunc.func,
137+
blocks: instr.value.loweredFunc.func.body.blocks,
138+
});
139+
}
140+
}
141+
if (
142+
block.terminal.kind === 'optional' &&
143+
!context.seenOptionals.has(block.id)
144+
) {
145+
traverseOptionalBlock(
146+
block as TBasicBlock<OptionalTerminal>,
147+
context,
148+
null,
149+
);
150+
}
151+
}
152+
}
128153
/**
129154
* Match the consequent and alternate blocks of an optional.
130155
* @returns propertyload computed by the consequent block, or null if the
@@ -369,10 +394,13 @@ function traverseOptionalBlock(
369394
},
370395
],
371396
};
372-
context.processedInstrsInOptional.add(
373-
matchConsequentResult.storeLocalInstrId,
397+
const processedInstrsInOptionalByFn = getOrInsertDefault(
398+
context.processedInstrsInOptional,
399+
context.currFn,
400+
new Set(),
374401
);
375-
context.processedInstrsInOptional.add(test.id);
402+
processedInstrsInOptionalByFn.add(matchConsequentResult.storeLocalInstrId);
403+
processedInstrsInOptionalByFn.add(test.id);
376404
context.temporariesReadInOptional.set(
377405
matchConsequentResult.consequentId,
378406
load,

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

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@ function findTemporariesUsedOutsideDeclaringScope(
176176
* $2 = LoadLocal 'foo'
177177
* $3 = CallExpression $2($1)
178178
* ```
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.
179+
* @param usedOutsideDeclaringScope is used to check the correctness of
180+
* reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
181+
* PropertyLoad in the returned temporaries map if reordering the read (from the
182+
* time-of-load to time-of-use) is valid.
181183
*
182184
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
183185
* (a proxy for mutable range) of the load source, later instructions may
@@ -215,7 +217,29 @@ export function collectTemporariesSidemap(
215217
fn: HIRFunction,
216218
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
217219
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
218-
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
220+
const temporaries = new Map();
221+
collectTemporariesSidemapImpl(
222+
fn,
223+
usedOutsideDeclaringScope,
224+
temporaries,
225+
false,
226+
);
227+
return temporaries;
228+
}
229+
230+
/**
231+
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
232+
* function and all nested functions.
233+
*
234+
* Note that IdentifierIds are currently unique, so we can use a single
235+
* Map<IdentifierId, ...> across all nested functions.
236+
*/
237+
function collectTemporariesSidemapImpl(
238+
fn: HIRFunction,
239+
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
240+
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
241+
isInnerFn: boolean,
242+
): void {
219243
for (const [_, block] of fn.body.blocks) {
220244
for (const instr of block.instructions) {
221245
const {value, lvalue} = instr;
@@ -224,27 +248,51 @@ export function collectTemporariesSidemap(
224248
);
225249

226250
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);
251+
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
252+
/**
253+
* All dependencies of a inner / nested function must have a base
254+
* identifier from the outermost component / hook. This is because the
255+
* compiler cannot break an inner function into multiple granular
256+
* scopes.
257+
*/
258+
const property = getProperty(
259+
value.object,
260+
value.property,
261+
false,
262+
temporaries,
263+
);
264+
temporaries.set(lvalue.identifier.id, property);
265+
}
234266
} else if (
235267
value.kind === 'LoadLocal' &&
236268
lvalue.identifier.name == null &&
237269
value.place.identifier.name !== null &&
238270
!usedOutside
239271
) {
240-
temporaries.set(lvalue.identifier.id, {
241-
identifier: value.place.identifier,
242-
path: [],
243-
});
272+
if (
273+
!isInnerFn ||
274+
fn.context.some(
275+
context => context.identifier.id === value.place.identifier.id,
276+
)
277+
) {
278+
temporaries.set(lvalue.identifier.id, {
279+
identifier: value.place.identifier,
280+
path: [],
281+
});
282+
}
283+
} else if (
284+
value.kind === 'FunctionExpression' ||
285+
value.kind === 'ObjectMethod'
286+
) {
287+
collectTemporariesSidemapImpl(
288+
value.loweredFunc.func,
289+
usedOutsideDeclaringScope,
290+
temporaries,
291+
true,
292+
);
244293
}
245294
}
246295
}
247-
return temporaries;
248296
}
249297

250298
function getProperty(
@@ -310,6 +358,12 @@ class Context {
310358
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
311359
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
312360

361+
/**
362+
* Tracks the traversal state. See Context.declare for explanation of why this
363+
* is needed.
364+
*/
365+
inInnerFn: boolean = false;
366+
313367
constructor(
314368
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
315369
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
@@ -360,12 +414,23 @@ class Context {
360414
}
361415

362416
/*
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.
417+
* Records where a value was declared, and optionally, the scope where the
418+
* value originated from. This is later used to determine if a dependency
419+
* should be added to a scope; if the current scope we are visiting is the
420+
* same scope where the value originates, it can't be a dependency on itself.
421+
*
422+
* Note that we do not track declarations or reassignments within inner
423+
* functions for the following reasons:
424+
* - inner functions cannot be split by scope boundaries and are guaranteed
425+
* to consume their own declarations
426+
* - reassignments within inner functions are tracked as context variables,
427+
* which already have extended mutable ranges to account for reassignments
428+
* - *most importantly* it's currently simply incorrect to compare inner
429+
* function instruction ids (tracked by `decl`) with outer ones (as stored
430+
* by root identifier mutable ranges).
367431
*/
368432
declare(identifier: Identifier, decl: Decl): void {
433+
if (this.inInnerFn) return;
369434
if (!this.#declarations.has(identifier.declarationId)) {
370435
this.#declarations.set(identifier.declarationId, decl);
371436
}
@@ -575,7 +640,10 @@ function collectDependencies(
575640
fn: HIRFunction,
576641
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
577642
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
578-
processedInstrsInOptional: ReadonlySet<InstructionId>,
643+
processedInstrsInOptional: ReadonlyMap<
644+
HIRFunction,
645+
ReadonlySet<InstructionId>
646+
>,
579647
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
580648
const context = new Context(usedOutsideDeclaringScope, temporaries);
581649

@@ -595,6 +663,12 @@ function collectDependencies(
595663

596664
const scopeTraversal = new ScopeBlockTraversal();
597665

666+
const shouldSkipInstructionDependencies = (
667+
fn: HIRFunction,
668+
id: InstructionId,
669+
): boolean => {
670+
return processedInstrsInOptional.get(fn)?.has(id) ?? false;
671+
};
598672
for (const [blockId, block] of fn.body.blocks) {
599673
scopeTraversal.recordScopes(block);
600674
const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
@@ -614,12 +688,12 @@ function collectDependencies(
614688
}
615689
}
616690
for (const instr of block.instructions) {
617-
if (!processedInstrsInOptional.has(instr.id)) {
691+
if (!shouldSkipInstructionDependencies(fn, instr.id)) {
618692
handleInstruction(instr, context);
619693
}
620694
}
621695

622-
if (!processedInstrsInOptional.has(block.terminal.id)) {
696+
if (!shouldSkipInstructionDependencies(fn, block.terminal.id)) {
623697
for (const place of eachTerminalOperand(block.terminal)) {
624698
context.visitOperand(place);
625699
}

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)