Skip to content

Commit bfc00d0

Browse files
committed
[compiler] Fallback for inferred effect dependencies
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 7213509 commit bfc00d0

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
@@ -12,6 +12,7 @@ import {
1212
BasicBlock,
1313
BlockId,
1414
DependencyPathEntry,
15+
FunctionExpression,
1516
GeneratedSource,
1617
getHookKind,
1718
HIRFunction,
@@ -23,6 +24,7 @@ import {
2324
PropertyLiteral,
2425
ReactiveScopeDependency,
2526
ScopeId,
27+
TInstruction,
2628
} from './HIR';
2729

2830
const DEBUG_PRINT = false;
@@ -120,6 +122,33 @@ export function collectHoistablePropertyLoads(
120122
});
121123
}
122124

125+
export function collectHoistablePropertyLoadsInInnerFn(
126+
fnInstr: TInstruction<FunctionExpression>,
127+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
128+
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
129+
): ReadonlyMap<BlockId, BlockInfo> {
130+
const fn = fnInstr.value.loweredFunc.func;
131+
const initialContext: CollectHoistablePropertyLoadsContext = {
132+
temporaries,
133+
knownImmutableIdentifiers: new Set(),
134+
hoistableFromOptionals,
135+
registry: new PropertyPathRegistry(),
136+
nestedFnImmutableContext: null,
137+
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
138+
? new Set()
139+
: getAssumedInvokedFunctions(fn),
140+
};
141+
const nestedFnImmutableContext = new Set(
142+
fn.context
143+
.filter(place =>
144+
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
145+
)
146+
.map(place => place.identifier.id),
147+
);
148+
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
149+
return collectHoistablePropertyLoadsImpl(fn, initialContext);
150+
}
151+
123152
type CollectHoistablePropertyLoadsContext = {
124153
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
125154
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
@@ -109,7 +109,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
109109
}
110110
}
111111

112-
function findTemporariesUsedOutsideDeclaringScope(
112+
export function findTemporariesUsedOutsideDeclaringScope(
113113
fn: HIRFunction,
114114
): ReadonlySet<DeclarationId> {
115115
/*
@@ -371,7 +371,7 @@ type Decl = {
371371
scope: Stack<ReactiveScope>;
372372
};
373373

374-
class Context {
374+
export class DependencyCollectionContext {
375375
#declarations: Map<DeclarationId, Decl> = new Map();
376376
#reassignments: Map<Identifier, Decl> = new Map();
377377

@@ -638,7 +638,10 @@ enum HIRValue {
638638
Terminal,
639639
}
640640

641-
function handleInstruction(instr: Instruction, context: Context): void {
641+
export function handleInstruction(
642+
instr: Instruction,
643+
context: DependencyCollectionContext,
644+
): void {
642645
const {id, value, lvalue} = instr;
643646
context.declare(lvalue.identifier, {
644647
id,
@@ -701,7 +704,7 @@ function collectDependencies(
701704
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
702705
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
703706
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
704-
const context = new Context(
707+
const context = new DependencyCollectionContext(
705708
usedOutsideDeclaringScope,
706709
temporaries,
707710
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
@@ -14,18 +14,30 @@ import {
1414
ScopeId,
1515
ReactiveScopeDependency,
1616
Place,
17+
ReactiveScope,
1718
ReactiveScopeDependencies,
19+
Terminal,
1820
isUseRefType,
1921
isSetStateType,
2022
isFireFunctionType,
23+
makeScopeId,
2124
} from '../HIR';
25+
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
26+
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
27+
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
2228
import {DEFAULT_EXPORT} from '../HIR/Environment';
2329
import {
2430
createTemporaryPlace,
2531
fixScopeAndIdentifierRanges,
2632
markInstructionIds,
2733
} from '../HIR/HIRBuilder';
34+
import {
35+
collectTemporariesSidemap,
36+
DependencyCollectionContext,
37+
handleInstruction,
38+
} from '../HIR/PropagateScopeDependenciesHIR';
2839
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
40+
import {empty} from '../Utils/Stack';
2941
import {getOrInsertWith} from '../Utils/utils';
3042

3143
/**
@@ -54,10 +66,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
5466
const autodepFnLoads = new Map<IdentifierId, number>();
5567
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
5668

57-
const scopeInfos = new Map<
58-
ScopeId,
59-
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
60-
>();
69+
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
6170

6271
const loadGlobals = new Set<IdentifierId>();
6372

@@ -71,19 +80,18 @@ export function inferEffectDependencies(fn: HIRFunction): void {
7180
const reactiveIds = inferReactiveIdentifiers(fn);
7281

7382
for (const [, block] of fn.body.blocks) {
74-
if (
75-
block.terminal.kind === 'scope' ||
76-
block.terminal.kind === 'pruned-scope'
77-
) {
83+
if (block.terminal.kind === 'scope') {
7884
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
79-
scopeInfos.set(block.terminal.scope.id, {
80-
pruned: block.terminal.kind === 'pruned-scope',
81-
deps: block.terminal.scope.dependencies,
82-
hasSingleInstr:
83-
scopeBlock.instructions.length === 1 &&
84-
scopeBlock.terminal.kind === 'goto' &&
85-
scopeBlock.terminal.block === block.terminal.fallthrough,
86-
});
85+
if (
86+
scopeBlock.instructions.length === 1 &&
87+
scopeBlock.terminal.kind === 'goto' &&
88+
scopeBlock.terminal.block === block.terminal.fallthrough
89+
) {
90+
scopeInfos.set(
91+
block.terminal.scope.id,
92+
block.terminal.scope.dependencies,
93+
);
94+
}
8795
}
8896
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
8997
for (const instr of block.instructions) {
@@ -165,30 +173,21 @@ export function inferEffectDependencies(fn: HIRFunction): void {
165173
fnExpr.lvalue.identifier.scope != null
166174
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
167175
: null;
168-
CompilerError.invariant(scopeInfo != null, {
169-
reason: 'Expected function expression scope to exist',
170-
loc: value.loc,
171-
});
172-
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
173-
/**
174-
* TODO: retry pipeline that ensures effect function expressions
175-
* are placed into their own scope
176-
*/
177-
CompilerError.throwTodo({
178-
reason:
179-
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
180-
loc: fnExpr.loc,
181-
});
176+
let minimalDeps: Set<ReactiveScopeDependency>;
177+
if (scopeInfo != null) {
178+
minimalDeps = new Set(scopeInfo);
179+
} else {
180+
minimalDeps = inferMinimalDependencies(fnExpr);
182181
}
183-
184182
/**
185183
* Step 1: push dependencies to the effect deps array
186184
*
187185
* Note that it's invalid to prune all non-reactive deps in this pass, see
188186
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
189187
* explanation.
190188
*/
191-
for (const dep of scopeInfo.deps) {
189+
190+
for (const dep of minimalDeps) {
192191
if (
193192
((isUseRefType(dep.identifier) ||
194193
isSetStateType(dep.identifier)) &&
@@ -340,3 +339,132 @@ function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
340339
}
341340
return reactiveIds;
342341
}
342+
343+
function inferMinimalDependencies(
344+
fnInstr: TInstruction<FunctionExpression>,
345+
): Set<ReactiveScopeDependency> {
346+
const fn = fnInstr.value.loweredFunc.func;
347+
348+
const temporaries = collectTemporariesSidemap(fn, new Set());
349+
const {
350+
hoistableObjects,
351+
processedInstrsInOptional,
352+
temporariesReadInOptional,
353+
} = collectOptionalChainSidemap(fn);
354+
355+
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
356+
fnInstr,
357+
temporaries,
358+
hoistableObjects,
359+
);
360+
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
361+
CompilerError.invariant(hoistableToFnEntry != null, {
362+
reason:
363+
'[InferEffectDependencies] Internal invariant broken: missing entry block',
364+
loc: fnInstr.loc,
365+
});
366+
367+
const dependencies = inferDependencies(
368+
fnInstr,
369+
new Map([...temporaries, ...temporariesReadInOptional]),
370+
processedInstrsInOptional,
371+
);
372+
373+
const tree = new ReactiveScopeDependencyTreeHIR(
374+
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
375+
);
376+
for (const dep of dependencies) {
377+
tree.addDependency({...dep});
378+
}
379+
380+
return tree.deriveMinimalDependencies();
381+
}
382+
383+
function inferDependencies(
384+
fnInstr: TInstruction<FunctionExpression>,
385+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
386+
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
387+
): Set<ReactiveScopeDependency> {
388+
const fn = fnInstr.value.loweredFunc.func;
389+
const context = new DependencyCollectionContext(
390+
new Set(),
391+
temporaries,
392+
processedInstrsInOptional,
393+
);
394+
for (const dep of fn.context) {
395+
context.declare(dep.identifier, {
396+
id: makeInstructionId(0),
397+
scope: empty(),
398+
});
399+
}
400+
const placeholderScope: ReactiveScope = {
401+
id: makeScopeId(0),
402+
range: {
403+
start: fnInstr.id,
404+
end: makeInstructionId(fnInstr.id + 1),
405+
},
406+
dependencies: new Set(),
407+
reassignments: new Set(),
408+
declarations: new Map(),
409+
earlyReturnValue: null,
410+
merged: new Set(),
411+
loc: GeneratedSource,
412+
};
413+
context.enterScope(placeholderScope);
414+
inferDependenciesInFn(fn, context, temporaries);
415+
context.exitScope(placeholderScope, false);
416+
const resultUnfiltered = context.deps.get(placeholderScope);
417+
CompilerError.invariant(resultUnfiltered != null, {
418+
reason:
419+
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
420+
loc: fn.loc,
421+
});
422+
423+
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
424+
const result = new Set<ReactiveScopeDependency>();
425+
for (const dep of resultUnfiltered) {
426+
if (fnContext.has(dep.identifier.id)) {
427+
result.add(dep);
428+
}
429+
}
430+
431+
return result;
432+
}
433+
434+
function inferDependenciesInFn(
435+
fn: HIRFunction,
436+
context: DependencyCollectionContext,
437+
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
438+
): void {
439+
for (const [, block] of fn.body.blocks) {
440+
// Record referenced optional chains in phis
441+
for (const phi of block.phis) {
442+
for (const operand of phi.operands) {
443+
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
444+
if (maybeOptionalChain) {
445+
context.visitDependency(maybeOptionalChain);
446+
}
447+
}
448+
}
449+
for (const instr of block.instructions) {
450+
if (
451+
instr.value.kind === 'FunctionExpression' ||
452+
instr.value.kind === 'ObjectMethod'
453+
) {
454+
context.declare(instr.lvalue.identifier, {
455+
id: instr.id,
456+
scope: context.currentScope,
457+
});
458+
/**
459+
* Recursively visit the inner function to extract dependencies
460+
*/
461+
const innerFn = instr.value.loweredFunc.func;
462+
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
463+
inferDependenciesInFn(innerFn, context, temporaries);
464+
});
465+
} else {
466+
handleInstruction(instr, context);
467+
}
468+
}
469+
}
470+
}
Lines changed: 39 additions & 0 deletions
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)