[compiler] Extract reusable logic for control dominators (#35146)
The next PR needs to check if a block is controlled by a value derived from a ref. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35146). * #35148 * #35147 * __->__ #35146
Joseph Savona committed
Nov 17, 2025 at 12:05 UTC
d6b1a0573b4c43e5222aee1de5b11a8e4b575c8e
2 files changed
+118
-97
compiler/packages/babel-plugin-react-compiler/src/Inference/ControlDominators.ts
new
+114
@@ -0,0 +1,114 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
9
+import {PostDominator} from '../HIR/Dominator';
10
+
11
+export type ControlDominators = (id: BlockId) => boolean;
12
+
13
+/**
14
+ * Returns an object that lazily calculates whether particular blocks are controlled
15
+ * by values of interest. Which values matter are up to the caller.
16
+ */
17
+export function createControlDominators(
18
+ fn: HIRFunction,
19
+ isControlVariable: (place: Place) => boolean,
20
+): ControlDominators {
21
+ const postDominators = computePostDominatorTree(fn, {
22
+ includeThrowsAsExitNode: false,
23
+ });
24
+ const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
25
+
26
+ function isControlledBlock(id: BlockId): boolean {
27
+ let controlBlocks = postDominatorFrontierCache.get(id);
28
+ if (controlBlocks === undefined) {
29
+ controlBlocks = postDominatorFrontier(fn, postDominators, id);
30
+ postDominatorFrontierCache.set(id, controlBlocks);
31
+ }
32
+ for (const blockId of controlBlocks) {
33
+ const controlBlock = fn.body.blocks.get(blockId)!;
34
+ switch (controlBlock.terminal.kind) {
35
+ case 'if':
36
+ case 'branch': {
37
+ if (isControlVariable(controlBlock.terminal.test)) {
38
+ return true;
39
+ }
40
+ break;
41
+ }
42
+ case 'switch': {
43
+ if (isControlVariable(controlBlock.terminal.test)) {
44
+ return true;
45
+ }
46
+ for (const case_ of controlBlock.terminal.cases) {
47
+ if (case_.test !== null && isControlVariable(case_.test)) {
48
+ return true;
49
+ }
50
+ }
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+
58
+ return isControlledBlock;
59
+}
60
+
61
+/*
62
+ * Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
63
+ * post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
64
+ * are the earliest blocks from which execution branches such that it may or may not reach the target block.
65
+ */
66
+function postDominatorFrontier(
67
+ fn: HIRFunction,
68
+ postDominators: PostDominator<BlockId>,
69
+ targetId: BlockId,
70
+): Set<BlockId> {
71
+ const visited = new Set<BlockId>();
72
+ const frontier = new Set<BlockId>();
73
+ const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
74
+ for (const blockId of [...targetPostDominators, targetId]) {
75
+ if (visited.has(blockId)) {
76
+ continue;
77
+ }
78
+ visited.add(blockId);
79
+ const block = fn.body.blocks.get(blockId)!;
80
+ for (const pred of block.preds) {
81
+ if (!targetPostDominators.has(pred)) {
82
+ // The predecessor does not always reach this block, we found an item on the frontier!
83
+ frontier.add(pred);
84
+ }
85
+ }
86
+ }
87
+ return frontier;
88
+}
89
+
90
+function postDominatorsOf(
91
+ fn: HIRFunction,
92
+ postDominators: PostDominator<BlockId>,
93
+ targetId: BlockId,
94
+): Set<BlockId> {
95
+ const result = new Set<BlockId>();
96
+ const visited = new Set<BlockId>();
97
+ const queue = [targetId];
98
+ while (queue.length) {
99
+ const currentId = queue.shift()!;
100
+ if (visited.has(currentId)) {
101
+ continue;
102
+ }
103
+ visited.add(currentId);
104
+ const current = fn.body.blocks.get(currentId)!;
105
+ for (const pred of current.preds) {
106
+ const predPostDominator = postDominators.get(pred) ?? pred;
107
+ if (predPostDominator === targetId || result.has(predPostDominator)) {
108
+ result.add(pred);
109
+ }
110
+ queue.push(pred);
111
+ }
112
+ }
113
+ return result;
114
+}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts
+4
-97
@@ -7,7 +7,6 @@
7
8
import {CompilerError} from '..';
9
import {
10
- BlockId,
10
Effect,
11
Environment,
12
HIRFunction,
@@ -15,14 +14,12 @@ import {
14
IdentifierId,
15
Instruction,
16
Place,
18
- computePostDominatorTree,
17
evaluatesToStableTypeOrContainer,
18
getHookKind,
19
isStableType,
20
isStableTypeContainer,
21
isUseOperator,
22
} from '../HIR';
25
-import {PostDominator} from '../HIR/Dominator';
23
import {
24
eachInstructionLValue,
25
eachInstructionOperand,
@@ -35,6 +32,7 @@ import {
32
} from '../ReactiveScopes/InferReactiveScopeVariables';
33
import DisjointSet from '../Utils/DisjointSet';
34
import {assertExhaustive} from '../Utils/utils';
35
+import {createControlDominators} from './ControlDominators';
36
37
/**
38
* Side map to track and propagate sources of stability (i.e. hook calls such as
@@ -212,45 +210,9 @@ export function inferReactivePlaces(fn: HIRFunction): void {
210
reactiveIdentifiers.markReactive(place);
211
}
212
215
- const postDominators = computePostDominatorTree(fn, {
216
- includeThrowsAsExitNode: false,
217
- });
218
- const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
219
-
220
- function isReactiveControlledBlock(id: BlockId): boolean {
221
- let controlBlocks = postDominatorFrontierCache.get(id);
222
- if (controlBlocks === undefined) {
223
- controlBlocks = postDominatorFrontier(fn, postDominators, id);
224
- postDominatorFrontierCache.set(id, controlBlocks);
225
- }
226
- for (const blockId of controlBlocks) {
227
- const controlBlock = fn.body.blocks.get(blockId)!;
228
- switch (controlBlock.terminal.kind) {
229
- case 'if':
230
- case 'branch': {
231
- if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
232
- return true;
233
- }
234
- break;
235
- }
236
- case 'switch': {
237
- if (reactiveIdentifiers.isReactive(controlBlock.terminal.test)) {
238
- return true;
239
- }
240
- for (const case_ of controlBlock.terminal.cases) {
241
- if (
242
- case_.test !== null &&
243
- reactiveIdentifiers.isReactive(case_.test)
244
- ) {
245
- return true;
246
- }
247
- }
248
- break;
249
- }
250
- }
251
- }
252
- return false;
253
- }
213
+ const isReactiveControlledBlock = createControlDominators(fn, place =>
214
+ reactiveIdentifiers.isReactive(place),
215
+ );
216
217
do {
218
for (const [, block] of fn.body.blocks) {
@@ -411,61 +373,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
373
propagateReactivityToInnerFunctions(fn, true);
374
}
375
414
-/*
415
- * Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
416
- * post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
417
- * are the earliest blocks from which execution branches such that it may or may not reach the target block.
418
- */
419
-function postDominatorFrontier(
420
- fn: HIRFunction,
421
- postDominators: PostDominator<BlockId>,
422
- targetId: BlockId,
423
-): Set<BlockId> {
424
- const visited = new Set<BlockId>();
425
- const frontier = new Set<BlockId>();
426
- const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
427
- for (const blockId of [...targetPostDominators, targetId]) {
428
- if (visited.has(blockId)) {
429
- continue;
430
- }
431
- visited.add(blockId);
432
- const block = fn.body.blocks.get(blockId)!;
433
- for (const pred of block.preds) {
434
- if (!targetPostDominators.has(pred)) {
435
- // The predecessor does not always reach this block, we found an item on the frontier!
436
- frontier.add(pred);
437
- }
438
- }
439
- }
440
- return frontier;
441
-}
442
-
443
-function postDominatorsOf(
444
- fn: HIRFunction,
445
- postDominators: PostDominator<BlockId>,
446
- targetId: BlockId,
447
-): Set<BlockId> {
448
- const result = new Set<BlockId>();
449
- const visited = new Set<BlockId>();
450
- const queue = [targetId];
451
- while (queue.length) {
452
- const currentId = queue.shift()!;
453
- if (visited.has(currentId)) {
454
- continue;
455
- }
456
- visited.add(currentId);
457
- const current = fn.body.blocks.get(currentId)!;
458
- for (const pred of current.preds) {
459
- const predPostDominator = postDominators.get(pred) ?? pred;
460
- if (predPostDominator === targetId || result.has(predPostDominator)) {
461
- result.add(pred);
462
- }
463
- queue.push(pred);
464
- }
465
- }
466
- return result;
467
-}
468
-
376
class ReactivityMap {
377
hasChanges: boolean = false;
378
reactive: Set<IdentifierId> = new Set();