[compiler] Switch to track setStates by aliasing and id instead of identifier names (#34973)
Summary: This makes the setState usage logic much more robust. We no longer rely on identifierName. Now we track when a setState is loaded into a new promoted identifier variable and track this in a map `setStateLoaded` map. For other types of instructions we consider the setState to be being used. In this case we record its usage into the `setStateUsages` map. Test Plan: We expect no changes in behavior for the current tests --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34973). * #35044 * #35020 * __->__ #34973 * #34972
Jorge Cabiedes committed
Nov 10, 2025 at 12:16 UTC
f76c3617e0f00c98656a36d1b4083b397c7638f2
1 file changed
+89
-41
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoDerivedComputationsInEffects_exp.ts
+89
-41
@@ -21,7 +21,6 @@ import {
21
isUseStateType,
22
BasicBlock,
23
isUseRefType,
24
- GeneratedSource,
24
SourceLocation,
25
} from '../HIR';
26
import {eachInstructionLValue, eachInstructionOperand} from '../HIR/visitors';
@@ -42,8 +41,8 @@ type ValidationContext = {
41
readonly errors: CompilerError;
42
readonly derivationCache: DerivationCache;
43
readonly effects: Set<HIRFunction>;
45
- readonly setStateCache: Map<string | undefined | null, Array<Place>>;
46
- readonly effectSetStateCache: Map<string | undefined | null, Array<Place>>;
44
+ readonly setStateLoads: Map<IdentifierId, IdentifierId | null>;
45
+ readonly setStateUsages: Map<IdentifierId, Set<SourceLocation>>;
46
};
47
48
class DerivationCache {
@@ -180,19 +179,16 @@ export function validateNoDerivedComputationsInEffects_exp(
179
const errors = new CompilerError();
180
const effects: Set<HIRFunction> = new Set();
181
183
- const setStateCache: Map<string | undefined | null, Array<Place>> = new Map();
184
- const effectSetStateCache: Map<
185
- string | undefined | null,
186
- Array<Place>
187
- > = new Map();
182
+ const setStateLoads: Map<IdentifierId, IdentifierId> = new Map();
183
+ const setStateUsages: Map<IdentifierId, Set<SourceLocation>> = new Map();
184
185
const context: ValidationContext = {
186
functions,
187
errors,
188
derivationCache,
189
effects,
194
- setStateCache,
195
- effectSetStateCache,
190
+ setStateLoads,
191
+ setStateUsages,
192
};
193
194
if (fn.fnType === 'Hook') {
@@ -281,11 +277,61 @@ function joinValue(
277
return 'fromPropsAndState';
278
}
279
280
+function getRootSetState(
281
+ key: IdentifierId,
282
+ loads: Map<IdentifierId, IdentifierId | null>,
283
+ visited: Set<IdentifierId> = new Set(),
284
+): IdentifierId | null {
285
+ if (visited.has(key)) {
286
+ return null;
287
+ }
288
+ visited.add(key);
289
+
290
+ const parentId = loads.get(key);
291
+
292
+ if (parentId === undefined) {
293
+ return null;
294
+ }
295
+
296
+ if (parentId === null) {
297
+ return key;
298
+ }
299
+
300
+ return getRootSetState(parentId, loads, visited);
301
+}
302
+
303
+function maybeRecordSetState(
304
+ instr: Instruction,
305
+ loads: Map<IdentifierId, IdentifierId | null>,
306
+ usages: Map<IdentifierId, Set<SourceLocation>>,
307
+): void {
308
+ for (const operand of eachInstructionLValue(instr)) {
309
+ if (
310
+ instr.value.kind === 'LoadLocal' &&
311
+ loads.has(instr.value.place.identifier.id)
312
+ ) {
313
+ loads.set(operand.identifier.id, instr.value.place.identifier.id);
314
+ } else {
315
+ if (isSetStateType(operand.identifier)) {
316
+ // this is a root setState
317
+ loads.set(operand.identifier.id, null);
318
+ }
319
+ }
320
+
321
+ const rootSetState = getRootSetState(operand.identifier.id, loads);
322
+ if (rootSetState !== null && usages.get(rootSetState) === undefined) {
323
+ usages.set(rootSetState, new Set([operand.loc]));
324
+ }
325
+ }
326
+}
327
+
328
function recordInstructionDerivations(
329
instr: Instruction,
330
context: ValidationContext,
331
isFirstPass: boolean,
332
): void {
333
+ maybeRecordSetState(instr, context.setStateLoads, context.setStateUsages);
334
+
335
let typeOfValue: TypeOfValue = 'ignored';
336
let isSource: boolean = false;
337
const sources: Set<IdentifierId> = new Set();
@@ -318,15 +364,13 @@ function recordInstructionDerivations(
364
}
365
366
for (const operand of eachInstructionOperand(instr)) {
321
- if (
322
- isSetStateType(operand.identifier) &&
323
- operand.loc !== GeneratedSource &&
324
- isFirstPass
325
- ) {
326
- if (context.setStateCache.has(operand.loc.identifierName)) {
327
- context.setStateCache.get(operand.loc.identifierName)!.push(operand);
328
- } else {
329
- context.setStateCache.set(operand.loc.identifierName, [operand]);
367
+ if (context.setStateLoads.has(operand.identifier.id)) {
368
+ const rootSetStateId = getRootSetState(
369
+ operand.identifier.id,
370
+ context.setStateLoads,
371
+ );
372
+ if (rootSetStateId !== null) {
373
+ context.setStateUsages.get(rootSetStateId)?.add(operand.loc);
374
}
375
}
376
@@ -532,11 +576,16 @@ function validateEffect(
576
577
const effectDerivedSetStateCalls: Array<{
578
value: CallExpression;
535
- loc: SourceLocation;
579
+ id: IdentifierId;
580
sourceIds: Set<IdentifierId>;
581
typeOfValue: TypeOfValue;
582
}> = [];
583
584
+ const effectSetStateUsages: Map<
585
+ IdentifierId,
586
+ Set<SourceLocation>
587
+ > = new Map();
588
+
589
const globals: Set<IdentifierId> = new Set();
590
for (const block of effectFunction.body.blocks.values()) {
591
for (const pred of block.preds) {
@@ -552,19 +601,16 @@ function validateEffect(
601
return;
602
}
603
604
+ maybeRecordSetState(instr, context.setStateLoads, effectSetStateUsages);
605
+
606
for (const operand of eachInstructionOperand(instr)) {
556
- if (
557
- isSetStateType(operand.identifier) &&
558
- operand.loc !== GeneratedSource
559
- ) {
560
- if (context.effectSetStateCache.has(operand.loc.identifierName)) {
561
- context.effectSetStateCache
562
- .get(operand.loc.identifierName)!
563
- .push(operand);
564
- } else {
565
- context.effectSetStateCache.set(operand.loc.identifierName, [
566
- operand,
567
- ]);
607
+ if (context.setStateLoads.has(operand.identifier.id)) {
608
+ const rootSetStateId = getRootSetState(
609
+ operand.identifier.id,
610
+ context.setStateLoads,
611
+ );
612
+ if (rootSetStateId !== null) {
613
+ effectSetStateUsages.get(rootSetStateId)?.add(operand.loc);
614
}
615
}
616
}
@@ -582,7 +628,7 @@ function validateEffect(
628
if (argMetadata !== undefined) {
629
effectDerivedSetStateCalls.push({
630
value: instr.value,
585
- loc: instr.value.callee.loc,
631
+ id: instr.value.callee.identifier.id,
632
sourceIds: argMetadata.sourcesIds,
633
typeOfValue: argMetadata.typeOfValue,
634
});
@@ -616,15 +662,17 @@ function validateEffect(
662
}
663
664
for (const derivedSetStateCall of effectDerivedSetStateCalls) {
665
+ const rootSetStateCall = getRootSetState(
666
+ derivedSetStateCall.id,
667
+ context.setStateLoads,
668
+ );
669
+
670
if (
620
- derivedSetStateCall.loc !== GeneratedSource &&
621
- context.effectSetStateCache.has(derivedSetStateCall.loc.identifierName) &&
622
- context.setStateCache.has(derivedSetStateCall.loc.identifierName) &&
623
- context.effectSetStateCache.get(derivedSetStateCall.loc.identifierName)!
624
- .length ===
625
- context.setStateCache.get(derivedSetStateCall.loc.identifierName)!
626
- .length -
627
- 1
671
+ rootSetStateCall !== null &&
672
+ effectSetStateUsages.has(rootSetStateCall) &&
673
+ context.setStateUsages.has(rootSetStateCall) &&
674
+ effectSetStateUsages.get(rootSetStateCall)!.size ===
675
+ context.setStateUsages.get(rootSetStateCall)!.size - 1
676
) {
677
const propsSet = new Set<string>();
678
const stateSet = new Set<string>();