[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 {