@samitouri / QOS-React-1 / commits / d6b1a0573b

[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();