[compiler] Infer deps configuration (#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.
Jordan Brown committed
Nov 22, 2024 at 17:19 UTC
2a9f4c04e54294b668e0a2ae11c1930c2e57b248
9 files changed
+168
-22
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+1
-1
@@ -356,7 +356,7 @@ function* runWithEnvironment(
356
});
357
358
if (env.config.inferEffectDependencies) {
359
- inferEffectDependencies(env, hir);
359
+ inferEffectDependencies(hir);
360
}
361
362
if (env.config.inlineJsxTransform) {
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+49
-2
@@ -242,9 +242,40 @@ const EnvironmentConfigSchema = z.object({
242
enableOptionalDependencies: z.boolean().default(true),
243
244
/**
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.
268
*/
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),
279
280
/**
281
* Enables inlining ReactElement object literals in place of JSX
@@ -614,6 +645,22 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
645
source: 'react-compiler-runtime',
646
importSpecifierName: 'useContext_withSelector',
647
},
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
+ ],
664
};
665
666
/**
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+33
-13
@@ -8,7 +8,6 @@ import {
8
HIRFunction,
9
IdentifierId,
10
Instruction,
11
- isUseEffectHookType,
11
makeInstructionId,
12
TInstruction,
13
InstructionId,
@@ -23,20 +22,33 @@ import {
22
markInstructionIds,
23
} from '../HIR/HIRBuilder';
24
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
25
+import {getOrInsertWith} from '../Utils/utils';
26
27
/**
28
* Infers reactive dependencies captured by useEffect lambdas and adds them as
29
* a second argument to the useEffect call if no dependency array is provided.
30
*/
31
-export function inferEffectDependencies(
32
- env: Environment,
33
- fn: HIRFunction,
34
-): void {
31
+export function inferEffectDependencies(fn: HIRFunction): void {
32
let hasRewrite = false;
33
const fnExpressions = new Map<
34
IdentifierId,
35
TInstruction<FunctionExpression>
36
>();
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
+
52
const scopeInfos = new Map<
53
ScopeId,
54
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
@@ -74,15 +86,23 @@ export function inferEffectDependencies(
86
lvalue.identifier.id,
87
instr as TInstruction<FunctionExpression>,
88
);
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
+ }
100
} else if (
101
/*
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
103
*/
104
value.kind === 'CallExpression' &&
84
- isUseEffectHookType(value.callee.identifier) &&
85
- value.args.length === 1 &&
105
+ autodepFnLoads.get(value.callee.identifier.id) === value.args.length &&
106
value.args[0].kind === 'Identifier'
107
) {
108
const fnExpr = fnExpressions.get(value.args[0].identifier.id);
@@ -132,7 +152,7 @@ export function inferEffectDependencies(
152
loc: GeneratedSource,
153
};
154
135
- const depsPlace = createTemporaryPlace(env, GeneratedSource);
155
+ const depsPlace = createTemporaryPlace(fn.env, GeneratedSource);
156
depsPlace.effect = Effect.Read;
157
158
newInstructions.push({
@@ -142,8 +162,8 @@ export function inferEffectDependencies(
162
value: deps,
163
});
164
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});
167
rewriteInstrs.set(instr.id, newInstructions);
168
}
169
}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-deps-custom-config.expect.md
new
+61
@@ -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
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-deps-custom-config.js
new
+9
@@ -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
+4
@@ -3,6 +3,8 @@
3
4
```javascript
5
// @inferEffectDependencies
6
+import {useEffect, useRef} from 'react';
7
+
8
const moduleNonReactive = 0;
9
10
function Component({foo, bar}) {
@@ -45,6 +47,8 @@ function Component({foo, bar}) {
47
48
```javascript
49
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
50
+import { useEffect, useRef } from "react";
51
+
52
const moduleNonReactive = 0;
53
54
function Component(t0) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.js
+2
@@ -1,4 +1,6 @@
1
// @inferEffectDependencies
2
+import {useEffect, useRef} from 'react';
3
+
4
const moduleNonReactive = 0;
5
6
function Component({foo, bar}) {
compiler/packages/snap/src/compiler.ts
-6
@@ -174,11 +174,6 @@ function makePluginOptions(
174
.filter(s => s.length > 0);
175
}
176
177
- let inferEffectDependencies = false;
178
- if (firstLine.includes('@inferEffectDependencies')) {
179
- inferEffectDependencies = true;
180
- }
181
-
177
let logs: Array<{filename: string | null; event: LoggerEvent}> = [];
178
let logger: Logger | null = null;
179
if (firstLine.includes('@logger')) {
@@ -202,7 +197,6 @@ function makePluginOptions(
197
hookPattern,
198
validatePreserveExistingMemoizationGuarantees,
199
validateBlocklistedImports,
205
- inferEffectDependencies,
200
},
201
compilationMode,
202
logger,
compiler/packages/snap/src/sprout/shared-runtime.ts
+9
@@ -363,6 +363,14 @@ export function useFragment(..._args: Array<any>): object {
363
};
364
}
365
366
+export function useSpecialEffect(
367
+ fn: () => any,
368
+ _secondArg: any,
369
+ deps: Array<any>,
370
+) {
371
+ React.useEffect(fn, deps);
372
+}
373
+
374
export function typedArrayPush<T>(array: Array<T>, item: T): void {
375
array.push(item);
376
}
@@ -370,4 +378,5 @@ export function typedArrayPush<T>(array: Array<T>, item: T): void {
378
export function typedLog(...values: Array<any>): void {
379
console.log(...values);
380
}
381
+
382
export default typedLog;