main
ts 389 lines 12.1 KB
Raw
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 {CompilerError} from '../CompilerError';
9 import {
10 DependencyPathEntry,
11 GeneratedSource,
12 Identifier,
13 PropertyLiteral,
14 ReactiveScopeDependency,
15 SourceLocation,
16 } from '../HIR';
17 import {printIdentifier} from '../HIR/PrintHIR';
18
19 /**
20 * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
21 * for detailed explanation.
22 */
23 export class ReactiveScopeDependencyTreeHIR {
24 /**
25 * Paths from which we can hoist PropertyLoads. If an `identifier`,
26 * `identifier.path`, or `identifier?.path` is in this map, it is safe to
27 * evaluate (non-optional) PropertyLoads from.
28 */
29 #hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
30 new Map();
31 #deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
32
33 /**
34 * @param hoistableObjects a set of paths from which we can safely evaluate
35 * PropertyLoads. Note that we expect these to not contain duplicates (e.g.
36 * both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges
37 * duplicates when traversing the CFG.
38 */
39 constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
40 for (const {path, identifier, reactive, loc} of hoistableObjects) {
41 let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
42 identifier,
43 reactive,
44 this.#hoistableObjects,
45 path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
46 loc,
47 );
48
49 for (let i = 0; i < path.length; i++) {
50 const prevAccessType = currNode.properties.get(
51 path[i].property,
52 )?.accessType;
53 const accessType =
54 i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
55 CompilerError.invariant(
56 prevAccessType == null || prevAccessType === accessType,
57 {
58 reason: 'Conflicting access types',
59 loc: GeneratedSource,
60 },
61 );
62 let nextNode = currNode.properties.get(path[i].property);
63 if (nextNode == null) {
64 nextNode = {
65 properties: new Map(),
66 accessType,
67 loc: path[i].loc,
68 };
69 currNode.properties.set(path[i].property, nextNode);
70 }
71 currNode = nextNode;
72 }
73 }
74 }
75
76 static #getOrCreateRoot<T extends string>(
77 identifier: Identifier,
78 reactive: boolean,
79 roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
80 defaultAccessType: T,
81 loc: SourceLocation,
82 ): TreeNode<T> {
83 // roots can always be accessed unconditionally in JS
84 let rootNode = roots.get(identifier);
85
86 if (rootNode === undefined) {
87 rootNode = {
88 properties: new Map(),
89 reactive,
90 accessType: defaultAccessType,
91 loc,
92 };
93 roots.set(identifier, rootNode);
94 } else {
95 CompilerError.invariant(reactive === rootNode.reactive, {
96 reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
97 description: `Identifier ${printIdentifier(identifier)}`,
98 loc: GeneratedSource,
99 });
100 }
101 return rootNode;
102 }
103
104 /**
105 * Join a dependency with `#hoistableObjects` to record the hoistable
106 * dependency. This effectively truncates @param dep to its maximal
107 * safe-to-evaluate subpath
108 */
109 addDependency(dep: ReactiveScopeDependency): void {
110 const {identifier, reactive, path, loc} = dep;
111 let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
112 identifier,
113 reactive,
114 this.#deps,
115 PropertyAccessType.UnconditionalAccess,
116 loc,
117 );
118 /**
119 * hoistableCursor is null if depCursor is not an object we can hoist
120 * property reads from otherwise, it represents the same node in the
121 * hoistable / cfg-informed tree
122 */
123 let hoistableCursor: HoistableNode | undefined =
124 this.#hoistableObjects.get(identifier);
125
126 // All properties read 'on the way' to a dependency are marked as 'access'
127 for (const entry of path) {
128 let nextHoistableCursor: HoistableNode | undefined;
129 let nextDepCursor: DependencyNode;
130 if (entry.optional) {
131 /**
132 * No need to check the access type since we can match both optional or non-optionals
133 * in the hoistable
134 * e.g. a?.b<rest> is hoistable if a.b<rest> is hoistable
135 */
136 if (hoistableCursor != null) {
137 nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
138 }
139
140 let accessType;
141 if (
142 hoistableCursor != null &&
143 hoistableCursor.accessType === 'NonNull'
144 ) {
145 /**
146 * For an optional chain dep `a?.b`: if the hoistable tree only
147 * contains `a`, we can keep either `a?.b` or 'a.b' as a dependency.
148 * (note that we currently do the latter for perf)
149 */
150 accessType = PropertyAccessType.UnconditionalAccess;
151 } else {
152 /**
153 * Given that it's safe to evaluate `depCursor` and optional load
154 * never throws, it's also safe to evaluate `depCursor?.entry`
155 */
156 accessType = PropertyAccessType.OptionalAccess;
157 }
158 nextDepCursor = makeOrMergeProperty(
159 depCursor,
160 entry.property,
161 accessType,
162 entry.loc,
163 );
164 } else if (
165 hoistableCursor != null &&
166 hoistableCursor.accessType === 'NonNull'
167 ) {
168 nextHoistableCursor = hoistableCursor.properties.get(entry.property);
169 nextDepCursor = makeOrMergeProperty(
170 depCursor,
171 entry.property,
172 PropertyAccessType.UnconditionalAccess,
173 entry.loc,
174 );
175 } else {
176 /**
177 * Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from
178 */
179 break;
180 }
181 depCursor = nextDepCursor;
182 hoistableCursor = nextHoistableCursor;
183 }
184 // mark the final node as a dependency
185 depCursor.accessType = merge(
186 depCursor.accessType,
187 PropertyAccessType.OptionalDependency,
188 );
189 }
190
191 deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
192 const results = new Set<ReactiveScopeDependency>();
193 for (const [rootId, rootNode] of this.#deps.entries()) {
194 collectMinimalDependenciesInSubtree(
195 rootNode,
196 rootNode.reactive,
197 rootId,
198 [],
199 results,
200 );
201 }
202
203 return results;
204 }
205
206 /*
207 * Prints dependency tree to string for debugging.
208 * @param includeAccesses
209 * @returns string representation of DependencyTree
210 */
211 printDeps(includeAccesses: boolean): string {
212 let res: Array<Array<string>> = [];
213
214 for (const [rootId, rootNode] of this.#deps.entries()) {
215 const rootResults = printSubtree(rootNode, includeAccesses).map(
216 result => `${printIdentifier(rootId)}.${result}`,
217 );
218 res.push(rootResults);
219 }
220 return res.flat().join('\n');
221 }
222
223 static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
224 const buf: Array<string> = [`tree() [`];
225 for (const [rootId, rootNode] of roots) {
226 buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
227 this.#debugImpl(buf, rootNode, 1);
228 }
229 buf.push(']');
230 return buf.length > 2 ? buf.join('\n') : buf.join('');
231 }
232
233 static #debugImpl<T extends string>(
234 buf: Array<string>,
235 node: TreeNode<T>,
236 depth: number = 0,
237 ): void {
238 for (const [property, childNode] of node.properties) {
239 buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
240 this.#debugImpl(buf, childNode, depth + 1);
241 }
242 }
243 }
244
245 /*
246 * Enum representing the access type of single property on a parent object.
247 * We distinguish on two independent axes:
248 * Optional / Unconditional:
249 * - whether this property is an optional load (within an optional chain)
250 * Access / Dependency:
251 * - Access: this property is read on the path of a dependency. We do not
252 * need to track change variables for accessed properties. Tracking accesses
253 * helps Forget do more granular dependency tracking.
254 * - Dependency: this property is read as a dependency and we must track changes
255 * to it for correctness.
256 * ```javascript
257 * // props.a is a dependency here and must be tracked
258 * deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
259 * // props.a is just an access here and does not need to be tracked
260 * deps: {props.a.b} ---> minimalDeps: {props.a.b}
261 * ```
262 */
263 enum PropertyAccessType {
264 OptionalAccess = 'OptionalAccess',
265 UnconditionalAccess = 'UnconditionalAccess',
266 OptionalDependency = 'OptionalDependency',
267 UnconditionalDependency = 'UnconditionalDependency',
268 }
269
270 function isOptional(access: PropertyAccessType): boolean {
271 return (
272 access === PropertyAccessType.OptionalAccess ||
273 access === PropertyAccessType.OptionalDependency
274 );
275 }
276 function isDependency(access: PropertyAccessType): boolean {
277 return (
278 access === PropertyAccessType.OptionalDependency ||
279 access === PropertyAccessType.UnconditionalDependency
280 );
281 }
282
283 function merge(
284 access1: PropertyAccessType,
285 access2: PropertyAccessType,
286 ): PropertyAccessType {
287 const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
288 const resultIsDependency = isDependency(access1) || isDependency(access2);
289
290 /*
291 * Straightforward merge.
292 * This can be represented as bitwise OR, but is written out for readability
293 *
294 * Observe that `UnconditionalAccess | ConditionalDependency` produces an
295 * unconditionally accessed conditional dependency. We currently use these
296 * as we use unconditional dependencies. (i.e. to codegen change variables)
297 */
298 if (resultIsUnconditional) {
299 if (resultIsDependency) {
300 return PropertyAccessType.UnconditionalDependency;
301 } else {
302 return PropertyAccessType.UnconditionalAccess;
303 }
304 } else {
305 // result is optional
306 if (resultIsDependency) {
307 return PropertyAccessType.OptionalDependency;
308 } else {
309 return PropertyAccessType.OptionalAccess;
310 }
311 }
312 }
313
314 type TreeNode<T extends string> = {
315 properties: Map<PropertyLiteral, TreeNode<T>>;
316 accessType: T;
317 loc: SourceLocation;
318 };
319 type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
320 type DependencyNode = TreeNode<PropertyAccessType>;
321
322 /**
323 * Recursively calculates minimal dependencies in a subtree.
324 * @param node DependencyNode representing a dependency subtree.
325 * @returns a minimal list of dependencies in this subtree.
326 */
327 function collectMinimalDependenciesInSubtree(
328 node: DependencyNode,
329 reactive: boolean,
330 rootIdentifier: Identifier,
331 path: Array<DependencyPathEntry>,
332 results: Set<ReactiveScopeDependency>,
333 ): void {
334 if (isDependency(node.accessType)) {
335 results.add({identifier: rootIdentifier, reactive, path, loc: node.loc});
336 } else {
337 for (const [childName, childNode] of node.properties) {
338 collectMinimalDependenciesInSubtree(
339 childNode,
340 reactive,
341 rootIdentifier,
342 [
343 ...path,
344 {
345 property: childName,
346 optional: isOptional(childNode.accessType),
347 loc: childNode.loc,
348 },
349 ],
350 results,
351 );
352 }
353 }
354 }
355
356 function printSubtree(
357 node: DependencyNode,
358 includeAccesses: boolean,
359 ): Array<string> {
360 const results: Array<string> = [];
361 for (const [propertyName, propertyNode] of node.properties) {
362 if (includeAccesses || isDependency(propertyNode.accessType)) {
363 results.push(`${propertyName} (${propertyNode.accessType})`);
364 }
365 const propertyResults = printSubtree(propertyNode, includeAccesses);
366 results.push(...propertyResults.map(result => `${propertyName}.${result}`));
367 }
368 return results;
369 }
370
371 function makeOrMergeProperty(
372 node: DependencyNode,
373 property: PropertyLiteral,
374 accessType: PropertyAccessType,
375 loc: SourceLocation,
376 ): DependencyNode {
377 let child = node.properties.get(property);
378 if (child == null) {
379 child = {
380 properties: new Map(),
381 accessType,
382 loc,
383 };
384 node.properties.set(property, child);
385 } else {
386 child.accessType = merge(child.accessType, accessType);
387 }
388 return child;
389 }