Skip to content

Commit 89e8875

Browse files
authored
[compiler] Fallback for inferred effect dependencies (facebook#32984)
When effect dependencies cannot be inferred due to memoization-related bailouts or unexpected mutable ranges (which currently often have to do with writes to refs), fall back to traversing the effect lambda itself. This fallback uses the same logic as PropagateScopeDependencies: 1. Collect a sidemap of loads and property loads 2. Find hoistable accesses from the control flow graph. Note that here, we currently take into account the mutable ranges of instructions (see `mutate-after-useeffect-granular-access` fixture) 3. Collect the set of property paths accessed by the effect 4. Merge to get the set of minimal dependencies
1 parent 2d0a5e3 commit 89e8875

12 files changed

+393
-78
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
BasicBlock,
2020
BlockId,
2121
DependencyPathEntry,
22+
FunctionExpression,
2223
GeneratedSource,
2324
getHookKind,
2425
HIRFunction,
@@ -30,6 +31,7 @@ import {
3031
PropertyLiteral,
3132
ReactiveScopeDependency,
3233
ScopeId,
34+
TInstruction,
3335
} from './HIR';
3436

3537
const DEBUG_PRINT = false;
@@ -127,6 +129,33 @@ export function collectHoistablePropertyLoads(
127129
});
128130
}
129131

132+
export function collectHoistablePropertyLoadsInInnerFn(
133+
fnInstr: TInstruction<FunctionExpression>,
134+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
135+
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
136+
): ReadonlyMap<BlockId, BlockInfo> {
137+
const fn = fnInstr.value.loweredFunc.func;
138+
const initialContext: CollectHoistablePropertyLoadsContext = {
139+
temporaries,
140+
knownImmutableIdentifiers: new Set(),
141+
hoistableFromOptionals,
142+
registry: new PropertyPathRegistry(),
143+
nestedFnImmutableContext: null,
144+
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
145+
? new Set()
146+
: getAssumedInvokedFunctions(fn),
147+
};
148+
const nestedFnImmutableContext = new Set(
149+
fn.context
150+
.filter(place =>
151+
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
152+
)
153+
.map(place => place.identifier.id),
154+
);
155+
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
156+
return collectHoistablePropertyLoadsImpl(fn, initialContext);
157+
}
158+
130159
type CollectHoistablePropertyLoadsContext = {
131160
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
132161
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
116116
}
117117
}
118118

119-
function findTemporariesUsedOutsideDeclaringScope(
119+
export function findTemporariesUsedOutsideDeclaringScope(
120120
fn: HIRFunction,
121121
): ReadonlySet<DeclarationId> {
122122
/*
@@ -378,7 +378,7 @@ type Decl = {
378378
scope: Stack<ReactiveScope>;
379379
};
380380

381-
class Context {
381+
export class DependencyCollectionContext {
382382
#declarations: Map<DeclarationId, Decl> = new Map();
383383
#reassignments: Map<Identifier, Decl> = new Map();
384384

@@ -645,7 +645,10 @@ enum HIRValue {
645645
Terminal,
646646
}
647647

648-
function handleInstruction(instr: Instruction, context: Context): void {
648+
export function handleInstruction(
649+
instr: Instruction,
650+
context: DependencyCollectionContext,
651+
): void {
649652
const {id, value, lvalue} = instr;
650653
context.declare(lvalue.identifier, {
651654
id,
@@ -708,7 +711,7 @@ function collectDependencies(
708711
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
709712
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
710713
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
711-
const context = new Context(
714+
const context = new DependencyCollectionContext(
712715
usedOutsideDeclaringScope,
713716
temporaries,
714717
processedInstrsInOptional,

compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,30 @@ import {
2222
ScopeId,
2323
ReactiveScopeDependency,
2424
Place,
25+
ReactiveScope,
2526
ReactiveScopeDependencies,
27+
Terminal,
2628
isUseRefType,
2729
isSetStateType,
2830
isFireFunctionType,
31+
makeScopeId,
2932
} from '../HIR';
33+
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
34+
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
35+
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
3036
import {DEFAULT_EXPORT} from '../HIR/Environment';
3137
import {
3238
createTemporaryPlace,
3339
fixScopeAndIdentifierRanges,
3440
markInstructionIds,
3541
} from '../HIR/HIRBuilder';
42+
import {
43+
collectTemporariesSidemap,
44+
DependencyCollectionContext,
45+
handleInstruction,
46+
} from '../HIR/PropagateScopeDependenciesHIR';
3647
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
48+
import {empty} from '../Utils/Stack';
3749
import {getOrInsertWith} from '../Utils/utils';
3850

3951
/**
@@ -62,10 +74,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
6274
const autodepFnLoads = new Map<IdentifierId, number>();
6375
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
6476

65-
const scopeInfos = new Map<
66-
ScopeId,
67-
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
68-
>();
77+
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
6978

7079
const loadGlobals = new Set<IdentifierId>();
7180

@@ -79,19 +88,18 @@ export function inferEffectDependencies(fn: HIRFunction): void {
7988
const reactiveIds = inferReactiveIdentifiers(fn);
8089

8190
for (const [, block] of fn.body.blocks) {
82-
if (
83-
block.terminal.kind === 'scope' ||
84-
block.terminal.kind === 'pruned-scope'
85-
) {
91+
if (block.terminal.kind === 'scope') {
8692
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
87-
scopeInfos.set(block.terminal.scope.id, {
88-
pruned: block.terminal.kind === 'pruned-scope',
89-
deps: block.terminal.scope.dependencies,
90-
hasSingleInstr:
91-
scopeBlock.instructions.length === 1 &&
92-
scopeBlock.terminal.kind === 'goto' &&
93-
scopeBlock.terminal.block === block.terminal.fallthrough,
94-
});
93+
if (
94+
scopeBlock.instructions.length === 1 &&
95+
scopeBlock.terminal.kind === 'goto' &&
96+
scopeBlock.terminal.block === block.terminal.fallthrough
97+
) {
98+
scopeInfos.set(
99+
block.terminal.scope.id,
100+
block.terminal.scope.dependencies,
101+
);
102+
}
95103
}
96104
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
97105
for (const instr of block.instructions) {
@@ -173,31 +181,22 @@ export function inferEffectDependencies(fn: HIRFunction): void {
173181
fnExpr.lvalue.identifier.scope != null
174182
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
175183
: null;
176-
CompilerError.invariant(scopeInfo != null, {
177-
reason: 'Expected function expression scope to exist',
178-
loc: value.loc,
179-
});
180-
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
181-
/**
182-
* TODO: retry pipeline that ensures effect function expressions
183-
* are placed into their own scope
184-
*/
185-
CompilerError.throwTodo({
186-
reason:
187-
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
188-
loc: fnExpr.loc,
189-
});
184+
let minimalDeps: Set<ReactiveScopeDependency>;
185+
if (scopeInfo != null) {
186+
minimalDeps = new Set(scopeInfo);
187+
} else {
188+
minimalDeps = inferMinimalDependencies(fnExpr);
190189
}
191-
192190
/**
193191
* Step 1: push dependencies to the effect deps array
194192
*
195193
* Note that it's invalid to prune all non-reactive deps in this pass, see
196194
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
197195
* explanation.
198196
*/
197+
199198
const usedDeps = [];
200-
for (const dep of scopeInfo.deps) {
199+
for (const dep of minimalDeps) {
201200
if (
202201
((isUseRefType(dep.identifier) ||
203202
isSetStateType(dep.identifier)) &&
@@ -422,3 +421,132 @@ function collectDepUsages(
422421

423422
return sourceLocations;
424423
}
424+
425+
function inferMinimalDependencies(
426+
fnInstr: TInstruction<FunctionExpression>,
427+
): Set<ReactiveScopeDependency> {
428+
const fn = fnInstr.value.loweredFunc.func;
429+
430+
const temporaries = collectTemporariesSidemap(fn, new Set());
431+
const {
432+
hoistableObjects,
433+
processedInstrsInOptional,
434+
temporariesReadInOptional,
435+
} = collectOptionalChainSidemap(fn);
436+
437+
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
438+
fnInstr,
439+
temporaries,
440+
hoistableObjects,
441+
);
442+
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
443+
CompilerError.invariant(hoistableToFnEntry != null, {
444+
reason:
445+
'[InferEffectDependencies] Internal invariant broken: missing entry block',
446+
loc: fnInstr.loc,
447+
});
448+
449+
const dependencies = inferDependencies(
450+
fnInstr,
451+
new Map([...temporaries, ...temporariesReadInOptional]),
452+
processedInstrsInOptional,
453+
);
454+
455+
const tree = new ReactiveScopeDependencyTreeHIR(
456+
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
457+
);
458+
for (const dep of dependencies) {
459+
tree.addDependency({...dep});
460+
}
461+
462+
return tree.deriveMinimalDependencies();
463+
}
464+
465+
function inferDependencies(
466+
fnInstr: TInstruction<FunctionExpression>,
467+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
468+
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
469+
): Set<ReactiveScopeDependency> {
470+
const fn = fnInstr.value.loweredFunc.func;
471+
const context = new DependencyCollectionContext(
472+
new Set(),
473+
temporaries,
474+
processedInstrsInOptional,
475+
);
476+
for (const dep of fn.context) {
477+
context.declare(dep.identifier, {
478+
id: makeInstructionId(0),
479+
scope: empty(),
480+
});
481+
}
482+
const placeholderScope: ReactiveScope = {
483+
id: makeScopeId(0),
484+
range: {
485+
start: fnInstr.id,
486+
end: makeInstructionId(fnInstr.id + 1),
487+
},
488+
dependencies: new Set(),
489+
reassignments: new Set(),
490+
declarations: new Map(),
491+
earlyReturnValue: null,
492+
merged: new Set(),
493+
loc: GeneratedSource,
494+
};
495+
context.enterScope(placeholderScope);
496+
inferDependenciesInFn(fn, context, temporaries);
497+
context.exitScope(placeholderScope, false);
498+
const resultUnfiltered = context.deps.get(placeholderScope);
499+
CompilerError.invariant(resultUnfiltered != null, {
500+
reason:
501+
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
502+
loc: fn.loc,
503+
});
504+
505+
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
506+
const result = new Set<ReactiveScopeDependency>();
507+
for (const dep of resultUnfiltered) {
508+
if (fnContext.has(dep.identifier.id)) {
509+
result.add(dep);
510+
}
511+
}
512+
513+
return result;
514+
}
515+
516+
function inferDependenciesInFn(
517+
fn: HIRFunction,
518+
context: DependencyCollectionContext,
519+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
520+
): void {
521+
for (const [, block] of fn.body.blocks) {
522+
// Record referenced optional chains in phis
523+
for (const phi of block.phis) {
524+
for (const operand of phi.operands) {
525+
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
526+
if (maybeOptionalChain) {
527+
context.visitDependency(maybeOptionalChain);
528+
}
529+
}
530+
}
531+
for (const instr of block.instructions) {
532+
if (
533+
instr.value.kind === 'FunctionExpression' ||
534+
instr.value.kind === 'ObjectMethod'
535+
) {
536+
context.declare(instr.lvalue.identifier, {
537+
id: instr.id,
538+
scope: context.currentScope,
539+
});
540+
/**
541+
* Recursively visit the inner function to extract dependencies
542+
*/
543+
const innerFn = instr.value.loweredFunc.func;
544+
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
545+
inferDependenciesInFn(innerFn, context, temporaries);
546+
});
547+
} else {
548+
handleInstruction(instr, context);
549+
}
550+
}
551+
}
552+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @inferEffectDependencies @panicThreshold(none)
6+
import {useEffect} from 'react';
7+
import {print} from 'shared-runtime';
8+
9+
function Component({foo}) {
10+
const arr = [];
11+
// Taking either arr[0].value or arr as a dependency is reasonable
12+
// as long as developers know what to expect.
13+
useEffect(() => print(arr[0].value));
14+
arr.push({value: foo});
15+
return arr;
16+
}
17+
18+
```
19+
20+
## Code
21+
22+
```javascript
23+
// @inferEffectDependencies @panicThreshold(none)
24+
import { useEffect } from "react";
25+
import { print } from "shared-runtime";
26+
27+
function Component(t0) {
28+
const { foo } = t0;
29+
const arr = [];
30+
31+
useEffect(() => print(arr[0].value), [arr[0].value]);
32+
arr.push({ value: foo });
33+
return arr;
34+
}
35+
36+
```
37+
38+
### Eval output
39+
(kind: exception) Fixture not implemented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @inferEffectDependencies @panicThreshold(none)
2+
import {useEffect} from 'react';
3+
import {print} from 'shared-runtime';
4+
5+
function Component({foo}) {
6+
const arr = [];
7+
// Taking either arr[0].value or arr as a dependency is reasonable
8+
// as long as developers know what to expect.
9+
useEffect(() => print(arr[0].value));
10+
arr.push({value: foo});
11+
return arr;
12+
}

0 commit comments

Comments
 (0)