@samitouri / QOS-React / commits / 4759161ed8

[compiler] Wrap ReactiveScopeDep path tokens in object

Previously the path of a ReactiveScopeDependency was `Array<string>`. We need to track whether each property access is optional or not, so as a first step we change this to `Array<{property: string}>`, making space for an additional property in a subsequent PR. ghstack-source-id: c5d38d72f6b9d084a5df69ad23178794468f5f8b Pull Request resolved: https://github.com/facebook/react/pull/30812

Joe Savona committed Aug 28, 2024 at 10:52 UTC 4759161ed8d8f77bad654b6c23a063c8ad8d4864
9 files changed +40 -94
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+10 -2
@@ -776,7 +776,7 @@ export type ManualMemoDependency = {
776 value: Place;
777 }
778 | {kind: 'Global'; identifierName: string};
779 - path: Array<string>;
779 + path: DependencyPath;
780 };
781
782 export type StartMemoize = {
@@ -1494,9 +1494,17 @@ export type ReactiveScopeDeclaration = {
1494
1495 export type ReactiveScopeDependency = {
1496 identifier: Identifier;
1497 - path: Array<string>;
1497 + path: DependencyPath;
1498 };
1499
1500 +export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
1501 + return (
1502 + a.length === b.length &&
1503 + a.every((item, ix) => item.property === b[ix].property)
1504 + );
1505 +}
1506 +export type DependencyPath = Array<{property: string}>;
1507 +
1508 /*
1509 * Simulated opaque type for BlockIds to prevent using normal numbers as block ids
1510 * accidentally.
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+1 -1
@@ -68,7 +68,7 @@ export function collectMaybeMemoDependencies(
68 if (object != null) {
69 return {
70 root: object.root,
71 - path: [...object.path, value.property],
71 + path: [...object.path, {property: value.property}],
72 };
73 }
74 break;
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+2 -2
@@ -1411,7 +1411,7 @@ function printDependencyComment(dependency: ReactiveScopeDependency): string {
1411 let name = identifier.name;
1412 if (dependency.path !== null) {
1413 for (const path of dependency.path) {
1414 - name += `.${path}`;
1414 + name += `.${path.property}`;
1415 }
1416 }
1417 return name;
@@ -1448,7 +1448,7 @@ function codegenDependency(
1448 let object: t.Expression = convertIdentifier(dependency.identifier);
1449 if (dependency.path !== null) {
1450 for (const path of dependency.path) {
1451 - object = t.memberExpression(object, t.identifier(path));
1451 + object = t.memberExpression(object, t.identifier(path.property));
1452 }
1453 }
1454 return object;
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts
+14 -56
@@ -14,20 +14,8 @@ import {assertExhaustive} from '../Utils/utils';
14 * We need to understand optional member expressions only when determining
15 * dependencies of a ReactiveScope (i.e. in {@link PropagateScopeDependencies}),
16 * hence why this type lives here (not in HIR.ts)
17 - *
18 - * {@link ReactiveScopePropertyDependency.optionalPath} is populated only if the Property
19 - * represents an optional member expression, and it represents the property path
20 - * loaded conditionally.
21 - * e.g. the member expr a.b.c?.d.e?.f is represented as
22 - * {
23 - * identifier: 'a';
24 - * path: ['b', 'c'],
25 - * optionalPath: ['d', 'e', 'f'].
26 - * }
17 */
28 -export type ReactiveScopePropertyDependency = ReactiveScopeDependency & {
29 - optionalPath: Array<string>;
30 -};
18 +export type ReactiveScopePropertyDependency = ReactiveScopeDependency;
19
20 /*
21 * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
@@ -69,59 +57,29 @@ export class ReactiveScopeDependencyTree {
57 }
58
59 add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void {
72 - const {path, optionalPath} = dep;
60 + const {path} = dep;
61 let currNode = this.#getOrCreateRoot(dep.identifier);
62
63 const accessType = inConditional
64 ? PropertyAccessType.ConditionalAccess
65 : PropertyAccessType.UnconditionalAccess;
66
79 - for (const property of path) {
67 + for (const item of path) {
68 // all properties read 'on the way' to a dependency are marked as 'access'
81 - let currChild = getOrMakeProperty(currNode, property);
69 + let currChild = getOrMakeProperty(currNode, item.property);
70 currChild.accessType = merge(currChild.accessType, accessType);
71 currNode = currChild;
72 }
73
86 - if (optionalPath.length === 0) {
87 - /*
88 - * If this property does not have a conditional path (i.e. a.b.c), the
89 - * final property node should be marked as an conditional/unconditional
90 - * `dependency` as based on control flow.
91 - */
92 - const depType = inConditional
93 - ? PropertyAccessType.ConditionalDependency
94 - : PropertyAccessType.UnconditionalDependency;
95 -
96 - currNode.accessType = merge(currNode.accessType, depType);
97 - } else {
98 - /*
99 - * Technically, we only depend on whether unconditional path `dep.path`
100 - * is nullish (not its actual value). As long as we preserve the nullthrows
101 - * behavior of `dep.path`, we can keep it as an access (and not promote
102 - * to a dependency).
103 - * See test `reduce-reactive-cond-memberexpr-join` for example.
104 - */
105 -
106 - /*
107 - * If this property has an optional path (i.e. a?.b.c), all optional
108 - * nodes should be marked accordingly.
109 - */
110 - for (const property of optionalPath) {
111 - let currChild = getOrMakeProperty(currNode, property);
112 - currChild.accessType = merge(
113 - currChild.accessType,
114 - PropertyAccessType.ConditionalAccess,
115 - );
116 - currNode = currChild;
117 - }
74 + /**
75 + * The final property node should be marked as an conditional/unconditional
76 + * `dependency` as based on control flow.
77 + */
78 + const depType = inConditional
79 + ? PropertyAccessType.ConditionalDependency
80 + : PropertyAccessType.UnconditionalDependency;
81
119 - // The final node should be marked as a conditional dependency.
120 - currNode.accessType = merge(
121 - currNode.accessType,
122 - PropertyAccessType.ConditionalDependency,
123 - );
124 - }
82 + currNode.accessType = merge(currNode.accessType, depType);
83 }
84
85 deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
@@ -294,7 +252,7 @@ type DependencyNode = {
252 };
253
254 type ReduceResultNode = {
297 - relativePath: Array<string>;
255 + relativePath: Array<{property: string}>;
256 accessType: PropertyAccessType;
257 };
258
@@ -325,7 +283,7 @@ function deriveMinimalDependenciesInSubtree(
283 const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
284 ({relativePath, accessType}) => {
285 return {
328 - relativePath: [childName, ...relativePath],
286 + relativePath: [{property: childName}, ...relativePath],
287 accessType,
288 };
289 },
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts
+1 -4
@@ -19,6 +19,7 @@ import {
19 ReactiveScopeDependency,
20 ReactiveStatement,
21 Type,
22 + areEqualPaths,
23 makeInstructionId,
24 } from '../HIR';
25 import {
@@ -525,10 +526,6 @@ function areEqualDependencies(
526 return true;
527 }
528
528 -export function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
529 - return a.length === b.length && a.every((item, ix) => item === b[ix]);
530 -}
531 -
529 /**
530 * Is this scope eligible for merging with subsequent scopes? In general this
531 * is only true if the scope's output values are guaranteed to change when its
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts
+1 -1
@@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
113 const identifier =
114 printIdentifier(dependency.identifier) +
115 printType(dependency.identifier.type);
116 - return `${identifier}${dependency.path.map(prop => `.${prop}`).join('')}`;
116 + return `${identifier}${dependency.path.map(token => `.${token.property}`).join('')}`;
117 }
118
119 export function printReactiveInstructions(
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts
+5 -22
@@ -7,6 +7,7 @@
7
8 import {CompilerError} from '../CompilerError';
9 import {
10 + areEqualPaths,
11 BlockId,
12 DeclarationId,
13 GeneratedSource,
@@ -35,7 +36,6 @@ import {
36 ReactiveScopeDependencyTree,
37 ReactiveScopePropertyDependency,
38 } from './DeriveMinimalDependencies';
38 -import {areEqualPaths} from './MergeReactiveScopesThatInvalidateTogether';
39 import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
40
41 /*
@@ -465,7 +465,6 @@ class Context {
465 #getProperty(
466 object: Place,
467 property: string,
468 - isConditional: boolean,
468 ): ReactiveScopePropertyDependency {
469 const resolvedObject = this.resolveTemporary(object);
470 const resolvedDependency = this.#properties.get(resolvedObject.identifier);
@@ -478,36 +477,21 @@ class Context {
477 objectDependency = {
478 identifier: resolvedObject.identifier,
479 path: [],
481 - optionalPath: [],
480 };
481 } else {
482 objectDependency = {
483 identifier: resolvedDependency.identifier,
484 path: [...resolvedDependency.path],
487 - optionalPath: [...resolvedDependency.optionalPath],
485 };
486 }
487
491 - // (2) Determine whether property is an optional access
492 - if (objectDependency.optionalPath.length > 0) {
493 - /*
494 - * If the base property dependency represents a optional member expression,
495 - * property is on the optionalPath (regardless of whether this PropertyLoad
496 - * itself was conditional)
497 - * e.g. for `a.b?.c.d`, `d` should be added to optionalPath
498 - */
499 - objectDependency.optionalPath.push(property);
500 - } else if (isConditional) {
501 - objectDependency.optionalPath.push(property);
502 - } else {
503 - objectDependency.path.push(property);
504 - }
488 + objectDependency.path.push({property});
489
490 return objectDependency;
491 }
492
493 declareProperty(lvalue: Place, object: Place, property: string): void {
510 - const nextDependency = this.#getProperty(object, property, false);
494 + const nextDependency = this.#getProperty(object, property);
495 this.#properties.set(lvalue.identifier, nextDependency);
496 }
497
@@ -516,7 +500,7 @@ class Context {
500 // ref.current access is not a valid dep
501 if (
502 isUseRefType(maybeDependency.identifier) &&
519 - maybeDependency.path.at(0) === 'current'
503 + maybeDependency.path.at(0)?.property === 'current'
504 ) {
505 return false;
506 }
@@ -577,7 +561,6 @@ class Context {
561 let dependency: ReactiveScopePropertyDependency = {
562 identifier: resolved.identifier,
563 path: [],
580 - optionalPath: [],
564 };
565 if (resolved.identifier.name === null) {
566 const propertyDependency = this.#properties.get(resolved.identifier);
@@ -589,7 +572,7 @@ class Context {
572 }
573
574 visitProperty(object: Place, property: string): void {
592 - const nextDependency = this.#getProperty(object, property, false);
575 + const nextDependency = this.#getProperty(object, property);
576 this.visitDependency(nextDependency);
577 }
578
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts
+2 -2
@@ -180,8 +180,8 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
180 [...scope.scope.dependencies].forEach(ident => {
181 let target: undefined | IdentifierId =
182 this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
183 - ident.path.forEach(key => {
184 - target &&= this.paths.get(target)?.get(key);
183 + ident.path.forEach(token => {
184 + target &&= this.paths.get(target)?.get(token.property);
185 });
186 if (target && this.map.get(target) === 'Create') {
187 scope.scope.dependencies.delete(ident);
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts
+4 -4
@@ -167,7 +167,7 @@ function compareDeps(
167
168 let isSubpath = true;
169 for (let i = 0; i < Math.min(inferred.path.length, source.path.length); i++) {
170 - if (inferred.path[i] !== source.path[i]) {
170 + if (inferred.path[i].property !== source.path[i].property) {
171 isSubpath = false;
172 break;
173 }
@@ -177,14 +177,14 @@ function compareDeps(
177 isSubpath &&
178 (source.path.length === inferred.path.length ||
179 (inferred.path.length >= source.path.length &&
180 - !inferred.path.includes('current')))
180 + !inferred.path.some(token => token.property === 'current')))
181 ) {
182 return CompareDependencyResult.Ok;
183 } else {
184 if (isSubpath) {
185 if (
186 - source.path.includes('current') ||
187 - inferred.path.includes('current')
186 + source.path.some(token => token.property === 'current') ||
187 + inferred.path.some(token => token.property === 'current')
188 ) {
189 return CompareDependencyResult.RefAccessDifference;
190 } else {