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