@samitouri / QOS-React / commits / c67e241c16

[compiler] Renames and no-op refactor for next PR

Rename for clarity: - `CollectHoistablePropertyLoads:Tree` -> `CollectHoistablePropertyLoads:PropertyPathRegistry` - `getPropertyLoadNode` -> `getOrCreateProperty` - `getOrCreateRoot` -> `getOrCreateIdentifier` - `PropertyLoadNode` -> `PropertyPathNode` Refactor to CFG joining logic for `CollectHoistablePropertyLoads`. We now write to the same set of inferredNonNullObjects when traversing from entry and exit blocks. This is more correct, as non-nulls inferred from a forward traversal should be included when computing the backward traversal (and vice versa). This fix is needed by an edge case in #31036 Added invariant into fixed-point iteration to terminate (instead of infinite looping). ghstack-source-id: 1e8eb2d566b649ede93de9a9c13dad09b96416a5 Pull Request resolved: https://github.com/facebook/react/pull/31036

Mofei Zhang committed Sep 30, 2024 at 12:24 UTC c67e241c1656dea4ece22a4ee5c25b6b36d0ca75
3 files changed +95 -108
compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts
+69 -91
@@ -4,6 +4,7 @@ import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils';
4 import {
5 BasicBlock,
6 BlockId,
7 + DependencyPathEntry,
8 GeneratedSource,
9 HIRFunction,
10 Identifier,
@@ -66,7 +67,9 @@ export function collectHoistablePropertyLoads(
67 fn: HIRFunction,
68 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
69 ): ReadonlyMap<ScopeId, BlockInfo> {
69 - const nodes = collectNonNullsInBlocks(fn, temporaries);
70 + const registry = new PropertyPathRegistry();
71 +
72 + const nodes = collectNonNullsInBlocks(fn, temporaries, registry);
73 propagateNonNull(fn, nodes);
74
75 const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
@@ -84,33 +87,33 @@ export function collectHoistablePropertyLoads(
87
88 export type BlockInfo = {
89 block: BasicBlock;
87 - assumedNonNullObjects: ReadonlySet<PropertyLoadNode>;
90 + assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
91 };
92
93 /**
91 - * Tree data structure to dedupe property loads (e.g. a.b.c)
94 + * PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
95 * and make computing sets intersections simpler.
96 */
97 type RootNode = {
95 - properties: Map<string, PropertyLoadNode>;
98 + properties: Map<string, PropertyPathNode>;
99 parent: null;
100 // Recorded to make later computations simpler
101 fullPath: ReactiveScopeDependency;
102 root: IdentifierId;
103 };
104
102 -type PropertyLoadNode =
105 +type PropertyPathNode =
106 | {
104 - properties: Map<string, PropertyLoadNode>;
105 - parent: PropertyLoadNode;
107 + properties: Map<string, PropertyPathNode>;
108 + parent: PropertyPathNode;
109 fullPath: ReactiveScopeDependency;
110 }
111 | RootNode;
112
110 -class Tree {
113 +class PropertyPathRegistry {
114 roots: Map<IdentifierId, RootNode> = new Map();
115
113 - getOrCreateRoot(identifier: Identifier): PropertyLoadNode {
116 + getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
117 /**
118 * Reads from a statically scoped variable are always safe in JS,
119 * with the exception of TDZ (not addressed by this pass).
@@ -132,49 +135,61 @@ class Tree {
135 return rootNode;
136 }
137
135 - static #getOrCreateProperty(
136 - node: PropertyLoadNode,
137 - property: string,
138 - ): PropertyLoadNode {
139 - let child = node.properties.get(property);
138 + static getOrCreatePropertyEntry(
139 + parent: PropertyPathNode,
140 + entry: DependencyPathEntry,
141 + ): PropertyPathNode {
142 + if (entry.optional) {
143 + CompilerError.throwTodo({
144 + reason: 'handle optional nodes',
145 + loc: GeneratedSource,
146 + });
147 + }
148 + let child = parent.properties.get(entry.property);
149 if (child == null) {
150 child = {
151 properties: new Map(),
143 - parent: node,
152 + parent: parent,
153 fullPath: {
145 - identifier: node.fullPath.identifier,
146 - path: node.fullPath.path.concat([{property, optional: false}]),
154 + identifier: parent.fullPath.identifier,
155 + path: parent.fullPath.path.concat(entry),
156 },
157 };
149 - node.properties.set(property, child);
158 + parent.properties.set(entry.property, child);
159 }
160 return child;
161 }
162
154 - getPropertyLoadNode(n: ReactiveScopeDependency): PropertyLoadNode {
163 + getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
164 /**
165 * We add ReactiveScopeDependencies according to instruction ordering,
166 * so all subpaths of a PropertyLoad should already exist
167 * (e.g. a.b is added before a.b.c),
168 */
160 - let currNode = this.getOrCreateRoot(n.identifier);
169 + let currNode = this.getOrCreateIdentifier(n.identifier);
170 if (n.path.length === 0) {
171 return currNode;
172 }
173 for (let i = 0; i < n.path.length - 1; i++) {
165 - currNode = assertNonNull(currNode.properties.get(n.path[i].property));
174 + currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
175 + currNode,
176 + n.path[i],
177 + );
178 }
179
168 - return Tree.#getOrCreateProperty(currNode, n.path.at(-1)!.property);
180 + return PropertyPathRegistry.getOrCreatePropertyEntry(
181 + currNode,
182 + n.path.at(-1)!,
183 + );
184 }
185 }
186
172 -function pushPropertyLoadNode(
173 - loadSource: Identifier,
174 - loadSourceNode: PropertyLoadNode,
187 +function addNonNullPropertyPath(
188 + source: Identifier,
189 + sourceNode: PropertyPathNode,
190 instrId: InstructionId,
191 knownImmutableIdentifiers: Set<IdentifierId>,
177 - result: Set<PropertyLoadNode>,
192 + result: Set<PropertyPathNode>,
193 ): void {
194 /**
195 * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
@@ -187,26 +202,22 @@ function pushPropertyLoadNode(
202 * See comment at top of function for why we track known immutable identifiers.
203 */
204 const isMutableAtInstr =
190 - loadSource.mutableRange.end > loadSource.mutableRange.start + 1 &&
191 - loadSource.scope != null &&
192 - inRange({id: instrId}, loadSource.scope.range);
205 + source.mutableRange.end > source.mutableRange.start + 1 &&
206 + source.scope != null &&
207 + inRange({id: instrId}, source.scope.range);
208 if (
209 !isMutableAtInstr ||
195 - knownImmutableIdentifiers.has(loadSourceNode.fullPath.identifier.id)
210 + knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id)
211 ) {
197 - let curr: PropertyLoadNode | null = loadSourceNode;
198 - while (curr != null) {
199 - result.add(curr);
200 - curr = curr.parent;
201 - }
212 + result.add(sourceNode);
213 }
214 }
215
216 function collectNonNullsInBlocks(
217 fn: HIRFunction,
218 temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
219 + registry: PropertyPathRegistry,
220 ): ReadonlyMap<BlockId, BlockInfo> {
209 - const tree = new Tree();
221 /**
222 * Due to current limitations of mutable range inference, there are edge cases in
223 * which we infer known-immutable values (e.g. props or hook params) to have a
@@ -227,18 +238,18 @@ function collectNonNullsInBlocks(
238 * Known non-null objects such as functional component props can be safely
239 * read from any block.
240 */
230 - const knownNonNullIdentifiers = new Set<PropertyLoadNode>();
241 + const knownNonNullIdentifiers = new Set<PropertyPathNode>();
242 if (
243 fn.fnType === 'Component' &&
244 fn.params.length > 0 &&
245 fn.params[0].kind === 'Identifier'
246 ) {
247 const identifier = fn.params[0].identifier;
237 - knownNonNullIdentifiers.add(tree.getOrCreateRoot(identifier));
248 + knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
249 }
250 const nodes = new Map<BlockId, BlockInfo>();
251 for (const [_, block] of fn.body.blocks) {
241 - const assumedNonNullObjects = new Set<PropertyLoadNode>(
252 + const assumedNonNullObjects = new Set<PropertyPathNode>(
253 knownNonNullIdentifiers,
254 );
255 for (const instr of block.instructions) {
@@ -247,9 +258,9 @@ function collectNonNullsInBlocks(
258 identifier: instr.value.object.identifier,
259 path: [],
260 };
250 - pushPropertyLoadNode(
261 + addNonNullPropertyPath(
262 instr.value.object.identifier,
252 - tree.getPropertyLoadNode(source),
263 + registry.getOrCreateProperty(source),
264 instr.id,
265 knownImmutableIdentifiers,
266 assumedNonNullObjects,
@@ -258,9 +269,9 @@ function collectNonNullsInBlocks(
269 const source = instr.value.value.identifier.id;
270 const sourceNode = temporaries.get(source);
271 if (sourceNode != null) {
261 - pushPropertyLoadNode(
272 + addNonNullPropertyPath(
273 instr.value.value.identifier,
263 - tree.getPropertyLoadNode(sourceNode),
274 + registry.getOrCreateProperty(sourceNode),
275 instr.id,
276 knownImmutableIdentifiers,
277 assumedNonNullObjects,
@@ -270,9 +281,9 @@ function collectNonNullsInBlocks(
281 const source = instr.value.object.identifier.id;
282 const sourceNode = temporaries.get(source);
283 if (sourceNode != null) {
273 - pushPropertyLoadNode(
284 + addNonNullPropertyPath(
285 instr.value.object.identifier,
275 - tree.getPropertyLoadNode(sourceNode),
286 + registry.getOrCreateProperty(sourceNode),
287 instr.id,
288 knownImmutableIdentifiers,
289 assumedNonNullObjects,
@@ -314,7 +325,6 @@ function propagateNonNull(
325 nodeId: BlockId,
326 direction: 'forward' | 'backward',
327 traversalState: Map<BlockId, 'active' | 'done'>,
317 - nonNullObjectsByBlock: Map<BlockId, ReadonlySet<PropertyLoadNode>>,
328 ): boolean {
329 /**
330 * Avoid re-visiting computed or currently active nodes, which can
@@ -345,7 +355,6 @@ function propagateNonNull(
355 pred,
356 direction,
357 traversalState,
348 - nonNullObjectsByBlock,
358 );
359 changed ||= neighborChanged;
360 }
@@ -374,38 +383,36 @@ function propagateNonNull(
383 const neighborAccesses = Set_intersect(
384 Array.from(neighbors)
385 .filter(n => traversalState.get(n) === 'done')
377 - .map(n => assertNonNull(nonNullObjectsByBlock.get(n))),
386 + .map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
387 );
388
380 - const prevObjects = assertNonNull(nonNullObjectsByBlock.get(nodeId));
381 - const newObjects = Set_union(prevObjects, neighborAccesses);
389 + const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
390 + const mergedObjects = Set_union(prevObjects, neighborAccesses);
391
383 - nonNullObjectsByBlock.set(nodeId, newObjects);
392 + assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
393 traversalState.set(nodeId, 'done');
385 - changed ||= prevObjects.size !== newObjects.size;
394 + changed ||= prevObjects.size !== mergedObjects.size;
395 return changed;
396 }
388 - const fromEntry = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
389 - const fromExit = new Map<BlockId, ReadonlySet<PropertyLoadNode>>();
390 - for (const [blockId, blockInfo] of nodes) {
391 - fromEntry.set(blockId, blockInfo.assumedNonNullObjects);
392 - fromExit.set(blockId, blockInfo.assumedNonNullObjects);
393 - }
397 const traversalState = new Map<BlockId, 'done' | 'active'>();
398 const reversedBlocks = [...fn.body.blocks];
399 reversedBlocks.reverse();
400
398 - let i = 0;
401 let changed;
402 + let i = 0;
403 do {
401 - i++;
404 + CompilerError.invariant(i++ < 100, {
405 + reason:
406 + '[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
407 + loc: GeneratedSource,
408 + });
409 +
410 changed = false;
411 for (const [blockId] of fn.body.blocks) {
412 const forwardChanged = recursivelyPropagateNonNull(
413 blockId,
414 'forward',
415 traversalState,
408 - fromEntry,
416 );
417 changed ||= forwardChanged;
418 }
@@ -415,43 +422,14 @@ function propagateNonNull(
422 blockId,
423 'backward',
424 traversalState,
418 - fromExit,
425 );
426 changed ||= backwardChanged;
427 }
428 traversalState.clear();
429 } while (changed);
424 -
425 - /**
426 - * TODO: validate against meta internal code, then remove in future PR.
427 - * Currently cannot come up with a case that requires fixed-point iteration.
428 - */
429 - CompilerError.invariant(i <= 2, {
430 - reason: 'require fixed-point iteration',
431 - description: `#iterations = ${i}`,
432 - loc: GeneratedSource,
433 - });
434 -
435 - CompilerError.invariant(
436 - fromEntry.size === fromExit.size && fromEntry.size === nodes.size,
437 - {
438 - reason:
439 - 'bad sizes after calculating fromEntry + fromExit ' +
440 - `${fromEntry.size} ${fromExit.size} ${nodes.size}`,
441 - loc: GeneratedSource,
442 - },
443 - );
444 -
445 - for (const [id, node] of nodes) {
446 - const assumedNonNullObjects = Set_union(
447 - assertNonNull(fromEntry.get(id)),
448 - assertNonNull(fromExit.get(id)),
449 - );
450 - node.assumedNonNullObjects = assumedNonNullObjects;
451 - }
430 }
431
454 -function assertNonNull<T extends NonNullable<U>, U>(
432 +export function assertNonNull<T extends NonNullable<U>, U>(
433 value: T | null | undefined,
434 source?: string,
435 ): T {
compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts
+16 -9
@@ -19,16 +19,17 @@ const ENABLE_DEBUG_INVARIANTS = true;
19 export class ReactiveScopeDependencyTreeHIR {
20 #roots: Map<Identifier, DependencyNode> = new Map();
21
22 - #getOrCreateRoot(identifier: Identifier, isNonNull: boolean): DependencyNode {
22 + #getOrCreateRoot(
23 + identifier: Identifier,
24 + accessType: PropertyAccessType,
25 + ): DependencyNode {
26 // roots can always be accessed unconditionally in JS
27 let rootNode = this.#roots.get(identifier);
28
29 if (rootNode === undefined) {
30 rootNode = {
31 properties: new Map(),
29 - accessType: isNonNull
30 - ? PropertyAccessType.NonNullAccess
31 - : PropertyAccessType.Access,
32 + accessType,
33 };
34 this.#roots.set(identifier, rootNode);
35 }
@@ -37,7 +38,7 @@ export class ReactiveScopeDependencyTreeHIR {
38
39 addDependency(dep: ReactiveScopePropertyDependency): void {
40 const {path} = dep;
40 - let currNode = this.#getOrCreateRoot(dep.identifier, false);
41 + let currNode = this.#getOrCreateRoot(dep.identifier, MIN_ACCESS_TYPE);
42
43 const accessType = PropertyAccessType.Access;
44
@@ -45,8 +46,11 @@ export class ReactiveScopeDependencyTreeHIR {
46
47 for (const property of path) {
48 // all properties read 'on the way' to a dependency are marked as 'access'
48 - let currChild = getOrMakeProperty(currNode, property.property);
49 - currChild.accessType = merge(currChild.accessType, accessType);
49 + let currChild = makeOrMergeProperty(
50 + currNode,
51 + property.property,
52 + accessType,
53 + );
54 currNode = currChild;
55 }
56
@@ -251,17 +255,20 @@ function printSubtree(
255 return results;
256 }
257
254 -function getOrMakeProperty(
258 +function makeOrMergeProperty(
259 node: DependencyNode,
260 property: string,
261 + accessType: PropertyAccessType,
262 ): DependencyNode {
263 let child = node.properties.get(property);
264 if (child == null) {
265 child = {
266 properties: new Map(),
262 - accessType: MIN_ACCESS_TYPE,
267 + accessType,
268 };
269 node.properties.set(property, child);
270 + } else {
271 + child.accessType = merge(child.accessType, accessType);
272 }
273 return child;
274 }
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+10 -8
@@ -874,13 +874,7 @@ export type InstructionValue =
874 };
875 loc: SourceLocation;
876 }
877 - | {
878 - kind: 'StoreLocal';
879 - lvalue: LValue;
880 - value: Place;
881 - type: t.FlowType | t.TSType | null;
882 - loc: SourceLocation;
883 - }
877 + | StoreLocal
878 | {
879 kind: 'StoreContext';
880 lvalue: {
@@ -1123,6 +1117,13 @@ export type Primitive = {
1117
1118 export type JSXText = {kind: 'JSXText'; value: string; loc: SourceLocation};
1119
1120 +export type StoreLocal = {
1121 + kind: 'StoreLocal';
1122 + lvalue: LValue;
1123 + value: Place;
1124 + type: t.FlowType | t.TSType | null;
1125 + loc: SourceLocation;
1126 +};
1127 export type PropertyLoad = {
1128 kind: 'PropertyLoad';
1129 object: Place;
@@ -1496,7 +1497,8 @@ export type ReactiveScopeDeclaration = {
1497 scope: ReactiveScope; // the scope in which the variable was originally declared
1498 };
1499
1499 -export type DependencyPath = Array<{property: string; optional: boolean}>;
1500 +export type DependencyPathEntry = {property: string; optional: boolean};
1501 +export type DependencyPath = Array<DependencyPathEntry>;
1502 export type ReactiveScopeDependency = {
1503 identifier: Identifier;
1504 path: DependencyPath;