[compiler] Inferred effect dependencies now include optional chains (#33326)
Inferred effect dependencies now include optional chains. This is a temporary solution while https://github.com/facebook/react/pull/32099 and its followups are worked on. Ideally, we should model reactive scope dependencies in the IR similarly to `ComputeIR` -- dependencies should be hoisted and all references rewritten to use the hoisted dependencies. ` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33326). * __->__ #33326 * #33325 * #32286
mofeiZ committed
May 22, 2025 at 16:14 UTC
0d072884f9201f645ae298936f2933970b73bec4
12 files changed
+762
-115
compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts
new
+283
@@ -0,0 +1,283 @@
1
+import {
2
+ Place,
3
+ ReactiveScopeDependency,
4
+ Identifier,
5
+ makeInstructionId,
6
+ InstructionKind,
7
+ GeneratedSource,
8
+ BlockId,
9
+ makeTemporaryIdentifier,
10
+ Effect,
11
+ GotoVariant,
12
+ HIR,
13
+} from './HIR';
14
+import {CompilerError} from '../CompilerError';
15
+import {Environment} from './Environment';
16
+import HIRBuilder from './HIRBuilder';
17
+import {lowerValueToTemporary} from './BuildHIR';
18
+
19
+type DependencyInstructions = {
20
+ place: Place;
21
+ value: HIR;
22
+ exitBlockId: BlockId;
23
+};
24
+
25
+export function buildDependencyInstructions(
26
+ dep: ReactiveScopeDependency,
27
+ env: Environment,
28
+): DependencyInstructions {
29
+ const builder = new HIRBuilder(env, {
30
+ entryBlockKind: 'value',
31
+ });
32
+ let dependencyValue: Identifier;
33
+ if (dep.path.every(path => !path.optional)) {
34
+ dependencyValue = writeNonOptionalDependency(dep, env, builder);
35
+ } else {
36
+ dependencyValue = writeOptionalDependency(dep, builder, null);
37
+ }
38
+
39
+ const exitBlockId = builder.terminate(
40
+ {
41
+ kind: 'unsupported',
42
+ loc: GeneratedSource,
43
+ id: makeInstructionId(0),
44
+ },
45
+ null,
46
+ );
47
+ return {
48
+ place: {
49
+ kind: 'Identifier',
50
+ identifier: dependencyValue,
51
+ effect: Effect.Freeze,
52
+ reactive: dep.reactive,
53
+ loc: GeneratedSource,
54
+ },
55
+ value: builder.build(),
56
+ exitBlockId,
57
+ };
58
+}
59
+
60
+/**
61
+ * Write instructions for a simple dependency (without optional chains)
62
+ */
63
+function writeNonOptionalDependency(
64
+ dep: ReactiveScopeDependency,
65
+ env: Environment,
66
+ builder: HIRBuilder,
67
+): Identifier {
68
+ const loc = dep.identifier.loc;
69
+ let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
70
+ builder.push({
71
+ lvalue: {
72
+ identifier: curr,
73
+ kind: 'Identifier',
74
+ effect: Effect.Mutate,
75
+ reactive: dep.reactive,
76
+ loc,
77
+ },
78
+ value: {
79
+ kind: 'LoadLocal',
80
+ place: {
81
+ identifier: dep.identifier,
82
+ kind: 'Identifier',
83
+ effect: Effect.Freeze,
84
+ reactive: dep.reactive,
85
+ loc,
86
+ },
87
+ loc,
88
+ },
89
+ id: makeInstructionId(1),
90
+ loc: loc,
91
+ });
92
+
93
+ /**
94
+ * Iteratively build up dependency instructions by reading from the last written
95
+ * instruction.
96
+ */
97
+ for (const path of dep.path) {
98
+ const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
99
+ builder.push({
100
+ lvalue: {
101
+ identifier: next,
102
+ kind: 'Identifier',
103
+ effect: Effect.Mutate,
104
+ reactive: dep.reactive,
105
+ loc,
106
+ },
107
+ value: {
108
+ kind: 'PropertyLoad',
109
+ object: {
110
+ identifier: curr,
111
+ kind: 'Identifier',
112
+ effect: Effect.Freeze,
113
+ reactive: dep.reactive,
114
+ loc,
115
+ },
116
+ property: path.property,
117
+ loc,
118
+ },
119
+ id: makeInstructionId(1),
120
+ loc: loc,
121
+ });
122
+ curr = next;
123
+ }
124
+ return curr;
125
+}
126
+
127
+/**
128
+ * Write a dependency into optional blocks.
129
+ *
130
+ * e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
131
+ * conditionally evaluates `c.d`.
132
+ */
133
+function writeOptionalDependency(
134
+ dep: ReactiveScopeDependency,
135
+ builder: HIRBuilder,
136
+ parentAlternate: BlockId | null,
137
+): Identifier {
138
+ const env = builder.environment;
139
+ /**
140
+ * Reserve an identifier which will be used to store the result of this
141
+ * dependency.
142
+ */
143
+ const dependencyValue: Place = {
144
+ kind: 'Identifier',
145
+ identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
146
+ effect: Effect.Mutate,
147
+ reactive: dep.reactive,
148
+ loc: GeneratedSource,
149
+ };
150
+
151
+ /**
152
+ * Reserve a block which is the fallthrough (and transitive successor) of this
153
+ * optional chain.
154
+ */
155
+ const continuationBlock = builder.reserve(builder.currentBlockKind());
156
+ let alternate;
157
+ if (parentAlternate != null) {
158
+ alternate = parentAlternate;
159
+ } else {
160
+ /**
161
+ * If an outermost alternate block has not been reserved, write one
162
+ *
163
+ * $N = Primitive undefined
164
+ * $M = StoreLocal $OptionalResult = $N
165
+ * goto fallthrough
166
+ */
167
+ alternate = builder.enter('value', () => {
168
+ const temp = lowerValueToTemporary(builder, {
169
+ kind: 'Primitive',
170
+ value: undefined,
171
+ loc: GeneratedSource,
172
+ });
173
+ lowerValueToTemporary(builder, {
174
+ kind: 'StoreLocal',
175
+ lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
176
+ value: {...temp},
177
+ type: null,
178
+ loc: GeneratedSource,
179
+ });
180
+ return {
181
+ kind: 'goto',
182
+ variant: GotoVariant.Break,
183
+ block: continuationBlock.id,
184
+ id: makeInstructionId(0),
185
+ loc: GeneratedSource,
186
+ };
187
+ });
188
+ }
189
+
190
+ // Reserve the consequent block, which is the successor of the test block
191
+ const consequent = builder.reserve('value');
192
+
193
+ let testIdentifier: Identifier | null = null;
194
+ const testBlock = builder.enter('value', () => {
195
+ const testDependency = {
196
+ ...dep,
197
+ path: dep.path.slice(0, dep.path.length - 1),
198
+ };
199
+ const firstOptional = dep.path.findIndex(path => path.optional);
200
+ CompilerError.invariant(firstOptional !== -1, {
201
+ reason:
202
+ '[ScopeDependencyUtils] Internal invariant broken: expected optional path',
203
+ loc: dep.identifier.loc,
204
+ description: null,
205
+ suggestions: null,
206
+ });
207
+ if (firstOptional === dep.path.length - 1) {
208
+ // Base case: the test block is simple
209
+ testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
210
+ } else {
211
+ // Otherwise, the test block is a nested optional chain
212
+ testIdentifier = writeOptionalDependency(
213
+ testDependency,
214
+ builder,
215
+ alternate,
216
+ );
217
+ }
218
+
219
+ return {
220
+ kind: 'branch',
221
+ test: {
222
+ identifier: testIdentifier,
223
+ effect: Effect.Freeze,
224
+ kind: 'Identifier',
225
+ loc: GeneratedSource,
226
+ reactive: dep.reactive,
227
+ },
228
+ consequent: consequent.id,
229
+ alternate,
230
+ id: makeInstructionId(0),
231
+ loc: GeneratedSource,
232
+ fallthrough: continuationBlock.id,
233
+ };
234
+ });
235
+
236
+ builder.enterReserved(consequent, () => {
237
+ CompilerError.invariant(testIdentifier !== null, {
238
+ reason: 'Satisfy type checker',
239
+ description: null,
240
+ loc: null,
241
+ suggestions: null,
242
+ });
243
+
244
+ lowerValueToTemporary(builder, {
245
+ kind: 'StoreLocal',
246
+ lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
247
+ value: lowerValueToTemporary(builder, {
248
+ kind: 'PropertyLoad',
249
+ object: {
250
+ identifier: testIdentifier,
251
+ kind: 'Identifier',
252
+ effect: Effect.Freeze,
253
+ reactive: dep.reactive,
254
+ loc: GeneratedSource,
255
+ },
256
+ property: dep.path.at(-1)!.property,
257
+ loc: GeneratedSource,
258
+ }),
259
+ type: null,
260
+ loc: GeneratedSource,
261
+ });
262
+ return {
263
+ kind: 'goto',
264
+ variant: GotoVariant.Break,
265
+ block: continuationBlock.id,
266
+ id: makeInstructionId(0),
267
+ loc: GeneratedSource,
268
+ };
269
+ });
270
+ builder.terminateWithContinuation(
271
+ {
272
+ kind: 'optional',
273
+ optional: dep.path.at(-1)!.optional,
274
+ test: testBlock,
275
+ fallthrough: continuationBlock.id,
276
+ id: makeInstructionId(0),
277
+ loc: GeneratedSource,
278
+ },
279
+ continuationBlock,
280
+ );
281
+
282
+ return dependencyValue.identifier;
283
+}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+191
-95
@@ -10,7 +10,6 @@ import {CompilerError, SourceLocation} from '..';
10
import {
11
ArrayExpression,
12
Effect,
13
- Environment,
13
FunctionExpression,
14
GeneratedSource,
15
HIRFunction,
@@ -29,6 +28,9 @@ import {
28
isSetStateType,
29
isFireFunctionType,
30
makeScopeId,
31
+ HIR,
32
+ BasicBlock,
33
+ BlockId,
34
} from '../HIR';
35
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
36
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
@@ -38,13 +40,20 @@ import {
40
createTemporaryPlace,
41
fixScopeAndIdentifierRanges,
42
markInstructionIds,
43
+ markPredecessors,
44
+ reversePostorderBlocks,
45
} from '../HIR/HIRBuilder';
46
import {
47
collectTemporariesSidemap,
48
DependencyCollectionContext,
49
handleInstruction,
50
} from '../HIR/PropagateScopeDependenciesHIR';
47
-import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
51
+import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
52
+import {
53
+ eachInstructionOperand,
54
+ eachTerminalOperand,
55
+ terminalFallthrough,
56
+} from '../HIR/visitors';
57
import {empty} from '../Utils/Stack';
58
import {getOrInsertWith} from '../Utils/utils';
59
@@ -53,7 +62,6 @@ import {getOrInsertWith} from '../Utils/utils';
62
* a second argument to the useEffect call if no dependency array is provided.
63
*/
64
export function inferEffectDependencies(fn: HIRFunction): void {
56
- let hasRewrite = false;
65
const fnExpressions = new Map<
66
IdentifierId,
67
TInstruction<FunctionExpression>
@@ -86,6 +94,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
94
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
95
*/
96
const reactiveIds = inferReactiveIdentifiers(fn);
97
+ const rewriteBlocks: Array<BasicBlock> = [];
98
99
for (const [, block] of fn.body.blocks) {
100
if (block.terminal.kind === 'scope') {
@@ -101,7 +110,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
110
);
111
}
112
}
104
- const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
113
+ const rewriteInstrs: Array<SpliceInfo> = [];
114
for (const instr of block.instructions) {
115
const {value, lvalue} = instr;
116
if (value.kind === 'FunctionExpression') {
@@ -165,7 +174,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
174
) {
175
// We have a useEffect call with no deps array, so we need to infer the deps
176
const effectDeps: Array<Place> = [];
168
- const newInstructions: Array<Instruction> = [];
177
const deps: ArrayExpression = {
178
kind: 'ArrayExpression',
179
elements: effectDeps,
@@ -196,24 +204,28 @@ export function inferEffectDependencies(fn: HIRFunction): void {
204
*/
205
206
const usedDeps = [];
199
- for (const dep of minimalDeps) {
207
+ for (const maybeDep of minimalDeps) {
208
if (
201
- ((isUseRefType(dep.identifier) ||
202
- isSetStateType(dep.identifier)) &&
203
- !reactiveIds.has(dep.identifier.id)) ||
204
- isFireFunctionType(dep.identifier)
209
+ ((isUseRefType(maybeDep.identifier) ||
210
+ isSetStateType(maybeDep.identifier)) &&
211
+ !reactiveIds.has(maybeDep.identifier.id)) ||
212
+ isFireFunctionType(maybeDep.identifier)
213
) {
214
// exclude non-reactive hook results, which will never be in a memo block
215
continue;
216
}
217
210
- const {place, instructions} = writeDependencyToInstructions(
218
+ const dep = truncateDepAtCurrent(maybeDep);
219
+ const {place, value, exitBlockId} = buildDependencyInstructions(
220
dep,
212
- reactiveIds.has(dep.identifier.id),
221
fn.env,
214
- fnExpr.loc,
222
);
216
- newInstructions.push(...instructions);
223
+ rewriteInstrs.push({
224
+ kind: 'block',
225
+ location: instr.id,
226
+ value,
227
+ exitBlockId: exitBlockId,
228
+ });
229
effectDeps.push(place);
230
usedDeps.push(dep);
231
}
@@ -234,27 +246,32 @@ export function inferEffectDependencies(fn: HIRFunction): void {
246
});
247
}
248
237
- newInstructions.push({
238
- id: makeInstructionId(0),
239
- loc: GeneratedSource,
240
- lvalue: {...depsPlace, effect: Effect.Mutate},
241
- value: deps,
242
- });
243
-
249
// Step 2: push the inferred deps array as an argument of the useEffect
250
+ rewriteInstrs.push({
251
+ kind: 'instr',
252
+ location: instr.id,
253
+ value: {
254
+ id: makeInstructionId(0),
255
+ loc: GeneratedSource,
256
+ lvalue: {...depsPlace, effect: Effect.Mutate},
257
+ value: deps,
258
+ },
259
+ });
260
value.args.push({...depsPlace, effect: Effect.Freeze});
246
- rewriteInstrs.set(instr.id, newInstructions);
261
fn.env.inferredEffectLocations.add(callee.loc);
262
} else if (loadGlobals.has(value.args[0].identifier.id)) {
263
// Global functions have no reactive dependencies, so we can insert an empty array
250
- newInstructions.push({
251
- id: makeInstructionId(0),
252
- loc: GeneratedSource,
253
- lvalue: {...depsPlace, effect: Effect.Mutate},
254
- value: deps,
264
+ rewriteInstrs.push({
265
+ kind: 'instr',
266
+ location: instr.id,
267
+ value: {
268
+ id: makeInstructionId(0),
269
+ loc: GeneratedSource,
270
+ lvalue: {...depsPlace, effect: Effect.Mutate},
271
+ value: deps,
272
+ },
273
});
274
value.args.push({...depsPlace, effect: Effect.Freeze});
257
- rewriteInstrs.set(instr.id, newInstructions);
275
fn.env.inferredEffectLocations.add(callee.loc);
276
}
277
} else if (
@@ -285,85 +302,164 @@ export function inferEffectDependencies(fn: HIRFunction): void {
302
}
303
}
304
}
288
- if (rewriteInstrs.size > 0) {
289
- hasRewrite = true;
290
- const newInstrs = [];
291
- for (const instr of block.instructions) {
292
- const newInstr = rewriteInstrs.get(instr.id);
293
- if (newInstr != null) {
294
- newInstrs.push(...newInstr, instr);
295
- } else {
296
- newInstrs.push(instr);
297
- }
298
- }
299
- block.instructions = newInstrs;
300
- }
305
+ rewriteSplices(block, rewriteInstrs, rewriteBlocks);
306
}
302
- if (hasRewrite) {
307
+
308
+ if (rewriteBlocks.length > 0) {
309
+ for (const block of rewriteBlocks) {
310
+ fn.body.blocks.set(block.id, block);
311
+ }
312
+
313
+ /**
314
+ * Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
315
+ * instructions.
316
+ */
317
+ reversePostorderBlocks(fn.body);
318
+ markPredecessors(fn.body);
319
// Renumber instructions and fix scope ranges
320
markInstructionIds(fn.body);
321
fixScopeAndIdentifierRanges(fn.body);
322
+
323
fn.env.hasInferredEffect = true;
324
}
325
}
326
310
-function writeDependencyToInstructions(
327
+function truncateDepAtCurrent(
328
dep: ReactiveScopeDependency,
312
- reactive: boolean,
313
- env: Environment,
314
- loc: SourceLocation,
315
-): {place: Place; instructions: Array<Instruction>} {
316
- const instructions: Array<Instruction> = [];
317
- let currValue = createTemporaryPlace(env, GeneratedSource);
318
- currValue.reactive = reactive;
319
- instructions.push({
320
- id: makeInstructionId(0),
321
- loc: GeneratedSource,
322
- lvalue: {...currValue, effect: Effect.Mutate},
323
- value: {
324
- kind: 'LoadLocal',
325
- place: {
326
- kind: 'Identifier',
327
- identifier: dep.identifier,
328
- effect: Effect.Capture,
329
- reactive,
330
- loc: loc,
331
- },
332
- loc: loc,
333
- },
334
- });
335
- for (const path of dep.path) {
336
- if (path.optional) {
337
- /**
338
- * TODO: instead of truncating optional paths, reuse
339
- * instructions from hoisted dependencies block(s)
340
- */
341
- break;
342
- }
343
- if (path.property === 'current') {
344
- /*
345
- * Prune ref.current accesses. This may over-capture for non-ref values with
346
- * a current property, but that's fine.
347
- */
348
- break;
329
+): ReactiveScopeDependency {
330
+ const idx = dep.path.findIndex(path => path.property === 'current');
331
+ if (idx === -1) {
332
+ return dep;
333
+ } else {
334
+ return {...dep, path: dep.path.slice(0, idx)};
335
+ }
336
+}
337
+
338
+type SpliceInfo =
339
+ | {kind: 'instr'; location: InstructionId; value: Instruction}
340
+ | {
341
+ kind: 'block';
342
+ location: InstructionId;
343
+ value: HIR;
344
+ exitBlockId: BlockId;
345
+ };
346
+
347
+function rewriteSplices(
348
+ originalBlock: BasicBlock,
349
+ splices: Array<SpliceInfo>,
350
+ rewriteBlocks: Array<BasicBlock>,
351
+): void {
352
+ if (splices.length === 0) {
353
+ return;
354
+ }
355
+ /**
356
+ * Splice instructions or value blocks into the original block.
357
+ * --- original block ---
358
+ * bb_original
359
+ * instr1
360
+ * ...
361
+ * instr2 <-- splice location
362
+ * instr3
363
+ * ...
364
+ * <original terminal>
365
+ *
366
+ * If there is more than one block in the splice, this means that we're
367
+ * splicing in a set of value-blocks of the following structure:
368
+ * --- blocks we're splicing in ---
369
+ * bb_entry:
370
+ * instrEntry
371
+ * ...
372
+ * <splice terminal> fallthrough=bb_exit
373
+ *
374
+ * bb1(value):
375
+ * ...
376
+ *
377
+ * bb_exit:
378
+ * instrExit
379
+ * ...
380
+ * <synthetic terminal>
381
+ *
382
+ *
383
+ * --- rewritten blocks ---
384
+ * bb_original
385
+ * instr1
386
+ * ... (original instructions)
387
+ * instr2
388
+ * instrEntry
389
+ * ... (spliced instructions)
390
+ * <splice terminal> fallthrough=bb_exit
391
+ *
392
+ * bb1(value):
393
+ * ...
394
+ *
395
+ * bb_exit:
396
+ * instrExit
397
+ * ... (spliced instructions)
398
+ * instr3
399
+ * ... (original instructions)
400
+ * <original terminal>
401
+ */
402
+ const originalInstrs = originalBlock.instructions;
403
+ let currBlock: BasicBlock = {...originalBlock, instructions: []};
404
+ rewriteBlocks.push(currBlock);
405
+
406
+ let cursor = 0;
407
+ for (const rewrite of splices) {
408
+ while (originalInstrs[cursor].id < rewrite.location) {
409
+ CompilerError.invariant(
410
+ originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
411
+ {
412
+ reason:
413
+ '[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
414
+ loc: originalInstrs[cursor].loc,
415
+ },
416
+ );
417
+ currBlock.instructions.push(originalInstrs[cursor]);
418
+ cursor++;
419
}
350
- const nextValue = createTemporaryPlace(env, GeneratedSource);
351
- nextValue.reactive = reactive;
352
- instructions.push({
353
- id: makeInstructionId(0),
354
- loc: GeneratedSource,
355
- lvalue: {...nextValue, effect: Effect.Mutate},
356
- value: {
357
- kind: 'PropertyLoad',
358
- object: {...currValue, effect: Effect.Capture},
359
- property: path.property,
360
- loc: loc,
361
- },
420
+ CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
421
+ reason:
422
+ '[InferEffectDependencies] Internal invariant broken: splice location not found',
423
+ loc: originalInstrs[cursor].loc,
424
});
363
- currValue = nextValue;
425
+
426
+ if (rewrite.kind === 'instr') {
427
+ currBlock.instructions.push(rewrite.value);
428
+ } else {
429
+ const {entry, blocks} = rewrite.value;
430
+ const entryBlock = blocks.get(entry)!;
431
+ // splice in all instructions from the entry block
432
+ currBlock.instructions.push(...entryBlock.instructions);
433
+ if (blocks.size > 1) {
434
+ /**
435
+ * We're splicing in a set of value-blocks, which means we need
436
+ * to push new blocks and update terminals.
437
+ */
438
+ CompilerError.invariant(
439
+ terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
440
+ {
441
+ reason:
442
+ '[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
443
+ loc: entryBlock.terminal.loc,
444
+ },
445
+ );
446
+ const originalTerminal = currBlock.terminal;
447
+ currBlock.terminal = entryBlock.terminal;
448
+
449
+ for (const [id, block] of blocks) {
450
+ if (id === entry) {
451
+ continue;
452
+ }
453
+ if (id === rewrite.exitBlockId) {
454
+ block.terminal = originalTerminal;
455
+ currBlock = block;
456
+ }
457
+ rewriteBlocks.push(block);
458
+ }
459
+ }
460
+ }
461
}
365
- currValue.effect = Effect.Freeze;
366
- return {place: currValue, instructions};
462
+ currBlock.instructions.push(...originalInstrs.slice(cursor));
463
}
464
465
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.expect.md
new
+58
@@ -0,0 +1,58 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
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
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: Component,
20
+ params: [{foo: 1}],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
29
+import { useEffect } from "react";
30
+import { print } from "shared-runtime";
31
+
32
+function Component(t0) {
33
+ const { foo } = t0;
34
+ const arr = [];
35
+
36
+ useEffect(() => print(arr[0]?.value), [arr[0]?.value]);
37
+ arr.push({ value: foo });
38
+ return arr;
39
+}
40
+
41
+export const FIXTURE_ENTRYPOINT = {
42
+ fn: Component,
43
+ params: [{ foo: 1 }],
44
+};
45
+
46
+```
47
+
48
+## Logs
49
+
50
+```
51
+{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
52
+{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":304},"end":{"line":9,"column":39,"index":341},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":326},"end":{"line":9,"column":27,"index":329},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]}
53
+{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
54
+```
55
+
56
+### Eval output
57
+(kind: ok) [{"value":1}]
58
+logs: [1]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.js
new
+17
@@ -0,0 +1,17 @@
1
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
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
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Component,
16
+ params: [{foo: 1}],
17
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md
+22
-3
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @inferEffectDependencies @panicThreshold:"none"
5
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
6
7
import {useEffect, useRef} from 'react';
8
import {print} from 'shared-runtime';
@@ -14,12 +14,17 @@ function Component({arrRef}) {
14
return arrRef;
15
}
16
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Component,
19
+ params: [{arrRef: {current: {val: 'initial ref value'}}}],
20
+};
21
+
22
```
23
24
## Code
25
26
```javascript
22
-// @inferEffectDependencies @panicThreshold:"none"
27
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
28
29
import { useEffect, useRef } from "react";
30
import { print } from "shared-runtime";
@@ -32,7 +37,21 @@ function Component(t0) {
37
return arrRef;
38
}
39
40
+export const FIXTURE_ENTRYPOINT = {
41
+ fn: Component,
42
+ params: [{ arrRef: { current: { val: "initial ref value" } } }],
43
+};
44
+
45
+```
46
+
47
+## Logs
48
+
49
+```
50
+{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"},"suggestions":null,"severity":"InvalidReact"}}
51
+{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":227},"end":{"line":8,"column":40,"index":265},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":249},"end":{"line":8,"column":30,"index":255},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
52
+{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53
```
54
55
### Eval output
38
-(kind: exception) Fixture not implemented
\ No newline at end of file
56
+(kind: ok) {"current":{"val":2}}
57
+logs: [{ val: 2 }]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.js
+6
-1
@@ -1,4 +1,4 @@
1
-// @inferEffectDependencies @panicThreshold:"none"
1
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
2
3
import {useEffect, useRef} from 'react';
4
import {print} from 'shared-runtime';
@@ -9,3 +9,8 @@ function Component({arrRef}) {
9
arrRef.current.val = 2;
10
return arrRef;
11
}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: Component,
15
+ params: [{arrRef: {current: {val: 'initial ref value'}}}],
16
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.expect.md
+27
-5
@@ -2,33 +2,55 @@
2
## Input
3
4
```javascript
5
-// @inferEffectDependencies @panicThreshold:"none"
5
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
6
import {useEffect} from 'react';
7
8
function Component({foo}) {
9
const arr = [];
10
- useEffect(() => arr.push(foo));
10
+ useEffect(() => {
11
+ arr.push(foo);
12
+ });
13
arr.push(2);
14
return arr;
15
}
16
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Component,
19
+ params: [{foo: 1}],
20
+};
21
+
22
```
23
24
## Code
25
26
```javascript
20
-// @inferEffectDependencies @panicThreshold:"none"
27
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
28
import { useEffect } from "react";
29
30
function Component(t0) {
31
const { foo } = t0;
32
const arr = [];
26
- useEffect(() => arr.push(foo), [arr, foo]);
33
+ useEffect(() => {
34
+ arr.push(foo);
35
+ }, [arr, foo]);
36
arr.push(2);
37
return arr;
38
}
39
40
+export const FIXTURE_ENTRYPOINT = {
41
+ fn: Component,
42
+ params: [{ foo: 1 }],
43
+};
44
+
45
+```
46
+
47
+## Logs
48
+
49
+```
50
+{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
51
+{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":149},"end":{"line":8,"column":4,"index":190},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":180},"end":{"line":7,"column":16,"index":183},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]}
52
+{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53
```
54
55
### Eval output
34
-(kind: exception) Fixture not implemented
\ No newline at end of file
56
+(kind: ok) [2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.js
+9
-2
@@ -1,9 +1,16 @@
1
-// @inferEffectDependencies @panicThreshold:"none"
1
+// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
2
import {useEffect} from 'react';
3
4
function Component({foo}) {
5
const arr = [];
6
- useEffect(() => arr.push(foo));
6
+ useEffect(() => {
7
+ arr.push(foo);
8
+ });
9
arr.push(2);
10
return arr;
11
}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: Component,
15
+ params: [{foo: 1}],
16
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-optional-chain-complex.expect.md
new
+99
@@ -0,0 +1,99 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @inferEffectDependencies
6
+import {useEffect} from 'react';
7
+import {print, shallowCopy} from 'shared-runtime';
8
+
9
+function ReactiveMemberExpr({cond, propVal}) {
10
+ const obj = {a: cond ? {b: propVal} : null, c: null};
11
+ const other = shallowCopy({a: {b: {c: {d: {e: {f: propVal + 1}}}}}});
12
+ const primitive = shallowCopy(propVal);
13
+ useEffect(() =>
14
+ print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f)
15
+ );
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: ReactiveMemberExpr,
20
+ params: [{cond: true, propVal: 1}],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
29
+import { useEffect } from "react";
30
+import { print, shallowCopy } from "shared-runtime";
31
+
32
+function ReactiveMemberExpr(t0) {
33
+ const $ = _c(13);
34
+ const { cond, propVal } = t0;
35
+ let t1;
36
+ if ($[0] !== cond || $[1] !== propVal) {
37
+ t1 = cond ? { b: propVal } : null;
38
+ $[0] = cond;
39
+ $[1] = propVal;
40
+ $[2] = t1;
41
+ } else {
42
+ t1 = $[2];
43
+ }
44
+ let t2;
45
+ if ($[3] !== t1) {
46
+ t2 = { a: t1, c: null };
47
+ $[3] = t1;
48
+ $[4] = t2;
49
+ } else {
50
+ t2 = $[4];
51
+ }
52
+ const obj = t2;
53
+ const t3 = propVal + 1;
54
+ let t4;
55
+ if ($[5] !== t3) {
56
+ t4 = shallowCopy({ a: { b: { c: { d: { e: { f: t3 } } } } } });
57
+ $[5] = t3;
58
+ $[6] = t4;
59
+ } else {
60
+ t4 = $[6];
61
+ }
62
+ const other = t4;
63
+ let t5;
64
+ if ($[7] !== propVal) {
65
+ t5 = shallowCopy(propVal);
66
+ $[7] = propVal;
67
+ $[8] = t5;
68
+ } else {
69
+ t5 = $[8];
70
+ }
71
+ const primitive = t5;
72
+ let t6;
73
+ if (
74
+ $[9] !== obj.a?.b ||
75
+ $[10] !== other?.a?.b?.c?.d?.e.f ||
76
+ $[11] !== primitive.a?.b.c?.d?.e.f
77
+ ) {
78
+ t6 = () =>
79
+ print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f);
80
+ $[9] = obj.a?.b;
81
+ $[10] = other?.a?.b?.c?.d?.e.f;
82
+ $[11] = primitive.a?.b.c?.d?.e.f;
83
+ $[12] = t6;
84
+ } else {
85
+ t6 = $[12];
86
+ }
87
+ useEffect(t6, [obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f]);
88
+}
89
+
90
+export const FIXTURE_ENTRYPOINT = {
91
+ fn: ReactiveMemberExpr,
92
+ params: [{ cond: true, propVal: 1 }],
93
+};
94
+
95
+```
96
+
97
+### Eval output
98
+(kind: ok)
99
+logs: [1,2,undefined]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-optional-chain-complex.js
new
+17
@@ -0,0 +1,17 @@
1
+// @inferEffectDependencies
2
+import {useEffect} from 'react';
3
+import {print, shallowCopy} from 'shared-runtime';
4
+
5
+function ReactiveMemberExpr({cond, propVal}) {
6
+ const obj = {a: cond ? {b: propVal} : null, c: null};
7
+ const other = shallowCopy({a: {b: {c: {d: {e: {f: propVal + 1}}}}}});
8
+ const primitive = shallowCopy(propVal);
9
+ useEffect(() =>
10
+ print(obj.a?.b, other?.a?.b?.c?.d?.e.f, primitive.a?.b.c?.d?.e.f)
11
+ );
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: ReactiveMemberExpr,
16
+ params: [{cond: true, propVal: 1}],
17
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-optional-chain.expect.md
+26
-7
@@ -6,12 +6,17 @@
6
import {useEffect} from 'react';
7
import {print} from 'shared-runtime';
8
9
-// TODO: take optional chains as dependencies
9
function ReactiveMemberExpr({cond, propVal}) {
11
- const obj = {a: cond ? {b: propVal} : null};
10
+ const obj = {a: cond ? {b: propVal} : null, c: null};
11
useEffect(() => print(obj.a?.b));
12
+ useEffect(() => print(obj.c?.d));
13
}
14
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: ReactiveMemberExpr,
17
+ params: [{cond: true, propVal: 1}],
18
+};
19
+
20
```
21
22
## Code
@@ -21,9 +26,8 @@ import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
26
import { useEffect } from "react";
27
import { print } from "shared-runtime";
28
24
-// TODO: take optional chains as dependencies
29
function ReactiveMemberExpr(t0) {
26
- const $ = _c(7);
30
+ const $ = _c(9);
31
const { cond, propVal } = t0;
32
let t1;
33
if ($[0] !== cond || $[1] !== propVal) {
@@ -36,7 +40,7 @@ function ReactiveMemberExpr(t0) {
40
}
41
let t2;
42
if ($[3] !== t1) {
39
- t2 = { a: t1 };
43
+ t2 = { a: t1, c: null };
44
$[3] = t1;
45
$[4] = t2;
46
} else {
@@ -51,10 +55,25 @@ function ReactiveMemberExpr(t0) {
55
} else {
56
t3 = $[6];
57
}
54
- useEffect(t3, [obj.a]);
58
+ useEffect(t3, [obj.a?.b]);
59
+ let t4;
60
+ if ($[7] !== obj.c?.d) {
61
+ t4 = () => print(obj.c?.d);
62
+ $[7] = obj.c?.d;
63
+ $[8] = t4;
64
+ } else {
65
+ t4 = $[8];
66
+ }
67
+ useEffect(t4, [obj.c?.d]);
68
}
69
70
+export const FIXTURE_ENTRYPOINT = {
71
+ fn: ReactiveMemberExpr,
72
+ params: [{ cond: true, propVal: 1 }],
73
+};
74
+
75
```
76
77
### Eval output
60
-(kind: exception) Fixture not implemented
\ No newline at end of file
78
+(kind: ok)
79
+logs: [1,undefined]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-optional-chain.js
+7
-2
@@ -2,8 +2,13 @@
2
import {useEffect} from 'react';
3
import {print} from 'shared-runtime';
4
5
-// TODO: take optional chains as dependencies
5
function ReactiveMemberExpr({cond, propVal}) {
7
- const obj = {a: cond ? {b: propVal} : null};
6
+ const obj = {a: cond ? {b: propVal} : null, c: null};
7
useEffect(() => print(obj.a?.b));
8
+ useEffect(() => print(obj.c?.d));
9
}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: ReactiveMemberExpr,
13
+ params: [{cond: true, propVal: 1}],
14
+};