@samitouri / QOS-React-1 / commits / 0d072884f9

[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 +};