Skip to content

Commit 2a9f4c0

Browse files
authored
[compiler] Infer deps configuration (facebook#31616)
Adds a way to configure how we insert deps for experimental purposes. ``` [ { module: 'react', imported: 'useEffect', numRequiredArgs: 1, }, { module: 'MyExperimentalEffectHooks', imported: 'useExperimentalEffect', numRequiredArgs: 2, }, ] ``` would insert dependencies for calls of `useEffect` imported from `react` if they have 1 argument and calls of useExperimentalEffect` from `MyExperimentalEffectHooks` if they have 2 arguments. The pushed dep array is appended to the arg list.
1 parent e3b7ef3 commit 2a9f4c0

File tree

9 files changed

+168
-22
lines changed

9 files changed

+168
-22
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ function* runWithEnvironment(
356356
});
357357

358358
if (env.config.inferEffectDependencies) {
359-
inferEffectDependencies(env, hir);
359+
inferEffectDependencies(hir);
360360
}
361361

362362
if (env.config.inlineJsxTransform) {

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,40 @@ const EnvironmentConfigSchema = z.object({
242242
enableOptionalDependencies: z.boolean().default(true),
243243

244244
/**
245-
* Enables inference and auto-insertion of effect dependencies. Still experimental.
245+
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
246+
* configurable module and import pairs to allow for user-land experimentation. For example,
247+
* [
248+
* {
249+
* module: 'react',
250+
* imported: 'useEffect',
251+
* numRequiredArgs: 1,
252+
* },{
253+
* module: 'MyExperimentalEffectHooks',
254+
* imported: 'useExperimentalEffect',
255+
* numRequiredArgs: 2,
256+
* },
257+
* ]
258+
* would insert dependencies for calls of `useEffect` imported from `react` and calls of
259+
* useExperimentalEffect` from `MyExperimentalEffectHooks`.
260+
*
261+
* `numRequiredArgs` tells the compiler the amount of arguments required to append a dependency
262+
* array to the end of the call. With the configuration above, we'd insert dependencies for
263+
* `useEffect` if it is only given a single argument and it would be appended to the argument list.
264+
*
265+
* numRequiredArgs must always be greater than 0, otherwise there is no function to analyze for dependencies
266+
*
267+
* Still experimental.
246268
*/
247-
inferEffectDependencies: z.boolean().default(false),
269+
inferEffectDependencies: z
270+
.nullable(
271+
z.array(
272+
z.object({
273+
function: ExternalFunctionSchema,
274+
numRequiredArgs: z.number(),
275+
}),
276+
),
277+
)
278+
.default(null),
248279

249280
/**
250281
* Enables inlining ReactElement object literals in place of JSX
@@ -614,6 +645,22 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
614645
source: 'react-compiler-runtime',
615646
importSpecifierName: 'useContext_withSelector',
616647
},
648+
inferEffectDependencies: [
649+
{
650+
function: {
651+
source: 'react',
652+
importSpecifierName: 'useEffect',
653+
},
654+
numRequiredArgs: 1,
655+
},
656+
{
657+
function: {
658+
source: 'shared-runtime',
659+
importSpecifierName: 'useSpecialEffect',
660+
},
661+
numRequiredArgs: 2,
662+
},
663+
],
617664
};
618665

619666
/**

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

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
HIRFunction,
99
IdentifierId,
1010
Instruction,
11-
isUseEffectHookType,
1211
makeInstructionId,
1312
TInstruction,
1413
InstructionId,
@@ -23,20 +22,33 @@ import {
2322
markInstructionIds,
2423
} from '../HIR/HIRBuilder';
2524
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
25+
import {getOrInsertWith} from '../Utils/utils';
2626

2727
/**
2828
* Infers reactive dependencies captured by useEffect lambdas and adds them as
2929
* a second argument to the useEffect call if no dependency array is provided.
3030
*/
31-
export function inferEffectDependencies(
32-
env: Environment,
33-
fn: HIRFunction,
34-
): void {
31+
export function inferEffectDependencies(fn: HIRFunction): void {
3532
let hasRewrite = false;
3633
const fnExpressions = new Map<
3734
IdentifierId,
3835
TInstruction<FunctionExpression>
3936
>();
37+
38+
const autodepFnConfigs = new Map<string, Map<string, number>>();
39+
for (const effectTarget of fn.env.config.inferEffectDependencies!) {
40+
const moduleTargets = getOrInsertWith(
41+
autodepFnConfigs,
42+
effectTarget.function.source,
43+
() => new Map<string, number>(),
44+
);
45+
moduleTargets.set(
46+
effectTarget.function.importSpecifierName,
47+
effectTarget.numRequiredArgs,
48+
);
49+
}
50+
const autodepFnLoads = new Map<IdentifierId, number>();
51+
4052
const scopeInfos = new Map<
4153
ScopeId,
4254
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
@@ -74,15 +86,23 @@ export function inferEffectDependencies(
7486
lvalue.identifier.id,
7587
instr as TInstruction<FunctionExpression>,
7688
);
89+
} else if (
90+
value.kind === 'LoadGlobal' &&
91+
value.binding.kind === 'ImportSpecifier'
92+
) {
93+
const moduleTargets = autodepFnConfigs.get(value.binding.module);
94+
if (moduleTargets != null) {
95+
const numRequiredArgs = moduleTargets.get(value.binding.imported);
96+
if (numRequiredArgs != null) {
97+
autodepFnLoads.set(lvalue.identifier.id, numRequiredArgs);
98+
}
99+
}
77100
} else if (
78101
/*
79-
* This check is not final. Right now we only look for useEffects without a dependency array.
80-
* This is likely not how we will ship this feature, but it is good enough for us to make progress
81-
* on the implementation and test it.
102+
* TODO: Handle method calls
82103
*/
83104
value.kind === 'CallExpression' &&
84-
isUseEffectHookType(value.callee.identifier) &&
85-
value.args.length === 1 &&
105+
autodepFnLoads.get(value.callee.identifier.id) === value.args.length &&
86106
value.args[0].kind === 'Identifier'
87107
) {
88108
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
@@ -132,7 +152,7 @@ export function inferEffectDependencies(
132152
loc: GeneratedSource,
133153
};
134154

135-
const depsPlace = createTemporaryPlace(env, GeneratedSource);
155+
const depsPlace = createTemporaryPlace(fn.env, GeneratedSource);
136156
depsPlace.effect = Effect.Read;
137157

138158
newInstructions.push({
@@ -142,8 +162,8 @@ export function inferEffectDependencies(
142162
value: deps,
143163
});
144164

145-
// Step 2: insert the deps array as an argument of the useEffect
146-
value.args[1] = {...depsPlace, effect: Effect.Freeze};
165+
// Step 2: push the inferred deps array as an argument of the useEffect
166+
value.args.push({...depsPlace, effect: Effect.Freeze});
147167
rewriteInstrs.set(instr.id, newInstructions);
148168
}
149169
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @inferEffectDependencies
6+
import {print, useSpecialEffect} from 'shared-runtime';
7+
8+
function CustomConfig({propVal}) {
9+
// Insertion
10+
useSpecialEffect(() => print(propVal), [propVal]);
11+
// No insertion
12+
useSpecialEffect(() => print(propVal), [propVal], [propVal]);
13+
}
14+
15+
```
16+
17+
## Code
18+
19+
```javascript
20+
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
21+
import { print, useSpecialEffect } from "shared-runtime";
22+
23+
function CustomConfig(t0) {
24+
const $ = _c(7);
25+
const { propVal } = t0;
26+
let t1;
27+
let t2;
28+
if ($[0] !== propVal) {
29+
t1 = () => print(propVal);
30+
t2 = [propVal];
31+
$[0] = propVal;
32+
$[1] = t1;
33+
$[2] = t2;
34+
} else {
35+
t1 = $[1];
36+
t2 = $[2];
37+
}
38+
useSpecialEffect(t1, t2, [propVal]);
39+
let t3;
40+
let t4;
41+
let t5;
42+
if ($[3] !== propVal) {
43+
t3 = () => print(propVal);
44+
t4 = [propVal];
45+
t5 = [propVal];
46+
$[3] = propVal;
47+
$[4] = t3;
48+
$[5] = t4;
49+
$[6] = t5;
50+
} else {
51+
t3 = $[4];
52+
t4 = $[5];
53+
t5 = $[6];
54+
}
55+
useSpecialEffect(t3, t4, t5);
56+
}
57+
58+
```
59+
60+
### Eval output
61+
(kind: exception) Fixture not implemented
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// @inferEffectDependencies
2+
import {print, useSpecialEffect} from 'shared-runtime';
3+
4+
function CustomConfig({propVal}) {
5+
// Insertion
6+
useSpecialEffect(() => print(propVal), [propVal]);
7+
// No insertion
8+
useSpecialEffect(() => print(propVal), [propVal], [propVal]);
9+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.expect.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
```javascript
55
// @inferEffectDependencies
6+
import {useEffect, useRef} from 'react';
7+
68
const moduleNonReactive = 0;
79

810
function Component({foo, bar}) {
@@ -45,6 +47,8 @@ function Component({foo, bar}) {
4547
4648
```javascript
4749
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
50+
import { useEffect, useRef } from "react";
51+
4852
const moduleNonReactive = 0;
4953

5054
function Component(t0) {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// @inferEffectDependencies
2+
import {useEffect, useRef} from 'react';
3+
24
const moduleNonReactive = 0;
35

46
function Component({foo, bar}) {

compiler/packages/snap/src/compiler.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,6 @@ function makePluginOptions(
174174
.filter(s => s.length > 0);
175175
}
176176

177-
let inferEffectDependencies = false;
178-
if (firstLine.includes('@inferEffectDependencies')) {
179-
inferEffectDependencies = true;
180-
}
181-
182177
let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
183178
let logger: Logger | null = null;
184179
if (firstLine.includes('@logger')) {
@@ -202,7 +197,6 @@ function makePluginOptions(
202197
hookPattern,
203198
validatePreserveExistingMemoizationGuarantees,
204199
validateBlocklistedImports,
205-
inferEffectDependencies,
206200
},
207201
compilationMode,
208202
logger,

compiler/packages/snap/src/sprout/shared-runtime.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,11 +363,20 @@ export function useFragment(..._args: Array<any>): object {
363363
};
364364
}
365365

366+
export function useSpecialEffect(
367+
fn: () => any,
368+
_secondArg: any,
369+
deps: Array<any>,
370+
) {
371+
React.useEffect(fn, deps);
372+
}
373+
366374
export function typedArrayPush<T>(array: Array<T>, item: T): void {
367375
array.push(item);
368376
}
369377

370378
export function typedLog(...values: Array<any>): void {
371379
console.log(...values);
372380
}
381+
373382
export default typedLog;

0 commit comments

Comments
 (0)