[rhir] move DeriveMinimalDependencies to its own file
--- Small refactor of reactive dependency logic, no behavioral change. - Moves ReactiveDependencyTree logic into `DeriveMinimalDependencies`. - this moves hides most helper functions + types 🥳 - made `ReactiveDependencyTree` a class - Changes `#dependencies` type: `Set<ReactiveScopeDep>` -> `ReactiveDependencyTree` - instead of collecting all dependencies into a tree in the end, we now eagerly join dependencies into the tree on `visitDep` - this is needed for the next PR in the stack, which relies on incremental merging
Mofei Zhang committed
Mar 6, 2023 at 16:33 UTC
25bfe728aac2e4226ba615c1a261d4eb5a895c73
2 files changed
+266
-290
compiler/forget/src/ReactiveScopes/DeriveMinimalDependencies.ts
new
+253
@@ -0,0 +1,253 @@
1
+import invariant from "invariant";
2
+import { Identifier, IdentifierId, ReactiveScopeDependency } from "../HIR";
3
+import { assertExhaustive } from "../Utils/utils";
4
+
5
+export type ReactiveScopeDependencyInfo = ReactiveScopeDependency & {
6
+ cond: boolean;
7
+};
8
+
9
+/**
10
+ * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
11
+ * dependencies, preserving granular accesses when possible.
12
+ *
13
+ * Correctness properties:
14
+ * - All dependencies to a ReactiveBlock must be tracked.
15
+ * We can always truncate a dependency's path to a subpath, due to Forget assuming
16
+ * deep immutability. If the value produced by a subpath has not changed, then
17
+ * dependency must have not changed.
18
+ * i.e. props.a === $[..] implies props.a.b === $[..]
19
+ *
20
+ * Note the inverse is not true, but this only means a false positive (we run the
21
+ * reactive block more than needed).
22
+ * i.e. props.a !== $[..] does not imply props.a.b !== $[..]
23
+ *
24
+ * - The dependencies of a finalized ReactiveBlock must be all safe to access
25
+ * unconditionally (i.e. preserve program semantics with respect to nullthrows).
26
+ * If a dependency is only accessed within a conditional, we must track the nearest
27
+ * unconditionally accessed subpath instead.
28
+ * @param initialDeps
29
+ * @returns
30
+ */
31
+export class ReactiveScopeDependencyTree {
32
+ #roots: Map<Identifier, DependencyNode> = new Map();
33
+
34
+ add(dep: ReactiveScopeDependencyInfo) {
35
+ let root = this.#roots.get(dep.identifier);
36
+ const path = dep.path ?? [];
37
+ if (root == null) {
38
+ // roots can always be accessed unconditionally in JS
39
+ root = {
40
+ properties: new Map(),
41
+ accessType: PropertyAccessType.UnconditionalAccess,
42
+ };
43
+ this.#roots.set(dep.identifier, root);
44
+ }
45
+ let currNode: DependencyNode = root;
46
+
47
+ const accessType = dep.cond
48
+ ? PropertyAccessType.ConditionalAccess
49
+ : PropertyAccessType.UnconditionalAccess;
50
+ const depType = dep.cond
51
+ ? PropertyAccessType.ConditionalDependency
52
+ : PropertyAccessType.UnconditionalDependency;
53
+
54
+ for (const property of path) {
55
+ // all properties read 'on the way' to a dependency are marked as 'access'
56
+ let currChild = currNode.properties.get(property);
57
+ if (currChild == null) {
58
+ currChild = {
59
+ properties: new Map(),
60
+ accessType,
61
+ };
62
+ currNode.properties.set(property, currChild);
63
+ } else {
64
+ currChild.accessType = merge(currChild.accessType, accessType);
65
+ }
66
+ currNode = currChild;
67
+ }
68
+
69
+ // final property read should be marked as `dependency`
70
+ currNode.accessType = merge(currNode.accessType, depType);
71
+ }
72
+
73
+ deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
74
+ const results = new Set<ReactiveScopeDependency>();
75
+ for (const [rootId, rootNode] of this.#roots.entries()) {
76
+ const deps = deriveMinimalDependenciesInSubtree(rootNode);
77
+ invariant(
78
+ deps.every(
79
+ (dep) => dep.accessType === PropertyAccessType.UnconditionalDependency
80
+ ),
81
+ "[PropagateScopeDependencies] All dependencies must be reduced to unconditional dependencies."
82
+ );
83
+
84
+ for (const dep of deps) {
85
+ results.add({
86
+ identifier: rootId,
87
+ path: dep.relativePath,
88
+ });
89
+ }
90
+ }
91
+
92
+ return results;
93
+ }
94
+}
95
+
96
+/**
97
+ * Enum representing the access type of single property on a parent object.
98
+ * We distinguish on two independent axes:
99
+ * Conditional / Unconditional:
100
+ * - whether this property is accessed unconditionally (within the ReactiveBlock)
101
+ * Access / Dependency:
102
+ * - Access: this property is read on the path of a dependency. We do not
103
+ * need to track change variables for accessed properties. Tracking accesses
104
+ * helps Forget do more granular dependency tracking.
105
+ * - Dependency: this property is read as a dependency and we must track changes
106
+ * to it for correctness.
107
+ *
108
+ * ```javascript
109
+ * // props.a is a dependency here and must be tracked
110
+ * deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
111
+ * // props.a is just an access here and does not need to be tracked
112
+ * deps: {props.a.b} ---> minimalDeps: {props.a.b}
113
+ * ```
114
+ */
115
+enum PropertyAccessType {
116
+ ConditionalAccess = "ConditionalAccess",
117
+ UnconditionalAccess = "UnconditionalAccess",
118
+ ConditionalDependency = "ConditionalDependency",
119
+ UnconditionalDependency = "UnconditionalDependency",
120
+}
121
+
122
+function isUnconditional(access: PropertyAccessType) {
123
+ return (
124
+ access === PropertyAccessType.UnconditionalAccess ||
125
+ access === PropertyAccessType.UnconditionalDependency
126
+ );
127
+}
128
+function isDependency(access: PropertyAccessType) {
129
+ return (
130
+ access === PropertyAccessType.ConditionalDependency ||
131
+ access === PropertyAccessType.UnconditionalDependency
132
+ );
133
+}
134
+
135
+function merge(
136
+ access1: PropertyAccessType,
137
+ access2: PropertyAccessType
138
+): PropertyAccessType {
139
+ const resultIsUnconditional =
140
+ isUnconditional(access1) || isUnconditional(access2);
141
+ const resultIsDependency = isDependency(access1) || isDependency(access2);
142
+
143
+ // Straightforward merge.
144
+ // This can be represented as bitwise OR, but is written out for readability
145
+ //
146
+ // Observe that `UnconditionalAccess | ConditionalDependency` produces an
147
+ // unconditionally accessed conditional dependency. We currently use these
148
+ // as we use unconditional dependencies. (i.e. to codegen change variables)
149
+ if (resultIsUnconditional) {
150
+ if (resultIsDependency) {
151
+ return PropertyAccessType.UnconditionalDependency;
152
+ } else {
153
+ return PropertyAccessType.UnconditionalAccess;
154
+ }
155
+ } else {
156
+ if (resultIsDependency) {
157
+ return PropertyAccessType.ConditionalDependency;
158
+ } else {
159
+ return PropertyAccessType.ConditionalAccess;
160
+ }
161
+ }
162
+}
163
+
164
+type DependencyNode = {
165
+ properties: Map<string, DependencyNode>;
166
+ accessType: PropertyAccessType;
167
+};
168
+
169
+type ReduceResultNode = {
170
+ relativePath: Array<string>;
171
+ accessType: PropertyAccessType;
172
+};
173
+
174
+const promoteUncondResult = [
175
+ {
176
+ relativePath: [],
177
+ accessType: PropertyAccessType.UnconditionalDependency,
178
+ },
179
+];
180
+
181
+const promoteCondResult = [
182
+ {
183
+ relativePath: [],
184
+ accessType: PropertyAccessType.ConditionalDependency,
185
+ },
186
+];
187
+
188
+/**
189
+ * Recursively calculates minimal dependencies in a subtree.
190
+ * @param dep DependencyNode representing a dependency subtree.
191
+ * @returns a minimal list of dependencies in this subtree.
192
+ */
193
+function deriveMinimalDependenciesInSubtree(
194
+ dep: DependencyNode
195
+): Array<ReduceResultNode> {
196
+ const results: Array<ReduceResultNode> = [];
197
+ for (const [childName, childNode] of dep.properties) {
198
+ const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
199
+ ({ relativePath, accessType }) => {
200
+ return {
201
+ relativePath: [childName, ...relativePath],
202
+ accessType,
203
+ };
204
+ }
205
+ );
206
+ results.push(...childResult);
207
+ }
208
+
209
+ switch (dep.accessType) {
210
+ case PropertyAccessType.UnconditionalDependency: {
211
+ return promoteUncondResult;
212
+ }
213
+ case PropertyAccessType.UnconditionalAccess: {
214
+ if (
215
+ results.every(
216
+ ({ accessType }) =>
217
+ accessType === PropertyAccessType.UnconditionalDependency
218
+ )
219
+ ) {
220
+ // all children are unconditional dependencies, return them to preserve granularity
221
+ return results;
222
+ } else {
223
+ // at least one child is accessed conditionally, so this node needs to be promoted to
224
+ // unconditional dependency
225
+ return promoteUncondResult;
226
+ }
227
+ }
228
+ case PropertyAccessType.ConditionalAccess:
229
+ case PropertyAccessType.ConditionalDependency: {
230
+ if (
231
+ results.every(
232
+ ({ accessType }) =>
233
+ accessType === PropertyAccessType.ConditionalDependency
234
+ )
235
+ ) {
236
+ // No children are accessed unconditionally, so we cannot promote this node to
237
+ // unconditional access.
238
+ // Truncate results of child nodes here, since we shouldn't access them anyways
239
+ return promoteCondResult;
240
+ } else {
241
+ // at least one child is accessed unconditionally, so this node can be promoted to
242
+ // unconditional dependency
243
+ return promoteUncondResult;
244
+ }
245
+ }
246
+ default: {
247
+ assertExhaustive(
248
+ dep.accessType,
249
+ "[PropgateScopeDependencies] Unhandled access type!"
250
+ );
251
+ }
252
+ }
253
+}
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+13
-290
@@ -5,7 +5,6 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import invariant from "invariant";
8
import {
9
Identifier,
10
IdentifierId,
@@ -25,6 +24,10 @@ import {
24
eachPatternOperand,
25
} from "../HIR/visitors";
26
import { assertExhaustive } from "../Utils/utils";
27
+import {
28
+ ReactiveScopeDependencyInfo,
29
+ ReactiveScopeDependencyTree,
30
+} from "./DeriveMinimalDependencies";
31
32
/**
33
* Infers the dependencies of each scope to include variables whose values
@@ -57,289 +60,11 @@ type Decl = {
60
61
type Scopes = Array<ReactiveScope>;
62
60
-// TODO(@mofeiZ): remove once we replace Context.#dependencies, #properties with tree
61
-// representation
62
-function areDependenciesEqual(
63
- dep1: ReactiveScopeDependencyInfo,
64
- dep2: ReactiveScopeDependencyInfo
65
-): boolean {
66
- if (dep1.identifier.id !== dep2.identifier.id || dep1.cond !== dep2.cond) {
67
- return false;
68
- }
69
- const dep1Path = dep1.path;
70
- const dep2Path = dep2.path;
71
-
72
- if (dep1Path === dep2Path) {
73
- // dep1Path and dep2Path might both be null (representing the empty path)
74
- return true;
75
- } else if (
76
- dep1Path === null ||
77
- dep2Path === null ||
78
- dep2Path.length !== dep1Path.length
79
- ) {
80
- return false;
81
- }
82
-
83
- return dep1Path.every((dep1Property, idx) => {
84
- return dep1Property === dep2Path[idx];
85
- });
86
-}
87
-
88
-type ReactiveScopeDependencyInfo = ReactiveScopeDependency & { cond: boolean };
89
-
90
-/**
91
- * Enum representing the access type of single property on a parent object.
92
- * We distinguish on two independent axes:
93
- * Conditional / Unconditional:
94
- * - whether this property is accessed unconditionally (within the ReactiveBlock)
95
- * Access / Dependency:
96
- * - Access: this property is read on the path of a dependency. We do not
97
- * need to track change variables for accessed properties. Tracking accesses
98
- * helps Forget do more granular dependency tracking.
99
- * - Dependency: this property is read as a dependency and we must track changes
100
- * to it for correctness.
101
- *
102
- * ```javascript
103
- * // props.a is a dependency here and must be tracked
104
- * deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
105
- * // props.a is just an access here and does not need to be tracked
106
- * deps: {props.a.b} ---> minimalDeps: {props.a.b}
107
- * ```
108
- */
109
-enum PropertyAccessType {
110
- ConditionalAccess = "ConditionalAccess",
111
- UnconditionalAccess = "UnconditionalAccess",
112
- ConditionalDependency = "ConditionalDependency",
113
- UnconditionalDependency = "UnconditionalDependency",
114
-}
115
-
116
-function isUnconditional(access: PropertyAccessType) {
117
- return (
118
- access === PropertyAccessType.UnconditionalAccess ||
119
- access === PropertyAccessType.UnconditionalDependency
120
- );
121
-}
122
-function isDependency(access: PropertyAccessType) {
123
- return (
124
- access === PropertyAccessType.ConditionalDependency ||
125
- access === PropertyAccessType.UnconditionalDependency
126
- );
127
-}
128
-
129
-function merge(
130
- access1: PropertyAccessType,
131
- access2: PropertyAccessType
132
-): PropertyAccessType {
133
- const resultIsUnconditional =
134
- isUnconditional(access1) || isUnconditional(access2);
135
- const resultIsDependency = isDependency(access1) || isDependency(access2);
136
-
137
- // Straightforward merge.
138
- // This can be represented as bitwise OR, but is written out for readability
139
- //
140
- // Observe that `UnconditionalAccess | ConditionalDependency` produces an
141
- // unconditionally accessed conditional dependency. We currently use these
142
- // as we use unconditional dependencies. (i.e. to codegen change variables)
143
- if (resultIsUnconditional) {
144
- if (resultIsDependency) {
145
- return PropertyAccessType.UnconditionalDependency;
146
- } else {
147
- return PropertyAccessType.UnconditionalAccess;
148
- }
149
- } else {
150
- if (resultIsDependency) {
151
- return PropertyAccessType.ConditionalDependency;
152
- } else {
153
- return PropertyAccessType.ConditionalAccess;
154
- }
155
- }
156
-}
157
-
158
-type DependencyNode = {
159
- properties: Map<string, DependencyNode>;
160
- accessType: PropertyAccessType;
161
-};
162
-
163
-type ReduceResultNode = {
164
- relativePath: Array<string>;
165
- accessType: PropertyAccessType;
166
-};
167
-
168
-const promoteUncondResult = [
169
- {
170
- relativePath: [],
171
- accessType: PropertyAccessType.UnconditionalDependency,
172
- },
173
-];
174
-
175
-const promoteCondResult = [
176
- {
177
- relativePath: [],
178
- accessType: PropertyAccessType.ConditionalDependency,
179
- },
180
-];
181
-
182
-/**
183
- * Recursively calculates minimal dependencies in a subtree.
184
- * @param dep DependencyNode representing a dependency subtree.
185
- * @returns a minimal list of dependencies in this subtree.
186
- */
187
-function deriveMinimalDependenciesInSubtree(
188
- dep: DependencyNode
189
-): Array<ReduceResultNode> {
190
- const results: Array<ReduceResultNode> = [];
191
- for (const [childName, childNode] of dep.properties) {
192
- const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
193
- ({ relativePath, accessType }) => {
194
- return {
195
- relativePath: [childName, ...relativePath],
196
- accessType,
197
- };
198
- }
199
- );
200
- results.push(...childResult);
201
- }
202
-
203
- switch (dep.accessType) {
204
- case PropertyAccessType.UnconditionalDependency: {
205
- return promoteUncondResult;
206
- }
207
- case PropertyAccessType.UnconditionalAccess: {
208
- if (
209
- results.every(
210
- ({ accessType }) =>
211
- accessType === PropertyAccessType.UnconditionalDependency
212
- )
213
- ) {
214
- // all children are unconditional dependencies, return them to preserve granularity
215
- return results;
216
- } else {
217
- // at least one child is accessed conditionally, so this node needs to be promoted to
218
- // unconditional dependency
219
- return promoteUncondResult;
220
- }
221
- }
222
- case PropertyAccessType.ConditionalAccess:
223
- case PropertyAccessType.ConditionalDependency: {
224
- if (
225
- results.every(
226
- ({ accessType }) =>
227
- accessType === PropertyAccessType.ConditionalDependency
228
- )
229
- ) {
230
- // No children are accessed unconditionally, so we cannot promote this node to
231
- // unconditional access.
232
- // Truncate results of child nodes here, since we shouldn't access them anyways
233
- return promoteCondResult;
234
- } else {
235
- // at least one child is accessed unconditionally, so this node can be promoted to
236
- // unconditional dependency
237
- return promoteUncondResult;
238
- }
239
- }
240
- default: {
241
- assertExhaustive(
242
- dep.accessType,
243
- "[PropgateScopeDependencies] Unhandled access type!"
244
- );
245
- }
246
- }
247
-}
248
-
249
-/**
250
- * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
251
- * dependencies, preserving granular accesses when possible.
252
- *
253
- * Correctness properties:
254
- * - All dependencies to a ReactiveBlock must be tracked.
255
- * We can always truncate a dependency's path to a subpath, due to Forget assuming
256
- * deep immutability. If the value produced by a subpath has not changed, then
257
- * dependency must have not changed.
258
- * i.e. props.a === $[..] implies props.a.b === $[..]
259
- *
260
- * Note the inverse is not true, but this only means a false positive (we run the
261
- * reactive block more than needed).
262
- * i.e. props.a !== $[..] does not imply props.a.b !== $[..]
263
- *
264
- * - The dependencies of a finalized ReactiveBlock must be all safe to access
265
- * unconditionally (i.e. preserve program semantics with respect to nullthrows).
266
- * If a dependency is only accessed within a conditional, we must track the nearest
267
- * unconditionally accessed subpath instead.
268
- * @param initialDeps
269
- * @returns
270
- */
271
-
272
-// TODO(@mofeiZ): change once we replace Context.#dependencies, #properties with tree
273
-// representation
274
-function deriveMinimalDependencies(
275
- initialDeps: Set<ReactiveScopeDependencyInfo>
276
-): Set<ReactiveScopeDependency> {
277
- const depRoots = new Map<Identifier, DependencyNode>();
278
-
279
- for (const dep of initialDeps) {
280
- let root = depRoots.get(dep.identifier);
281
- const path = dep.path ?? [];
282
- if (root == null) {
283
- // roots can always be accessed unconditionally in JS
284
- root = {
285
- properties: new Map(),
286
- accessType: PropertyAccessType.UnconditionalAccess,
287
- };
288
- depRoots.set(dep.identifier, root);
289
- }
290
- let currNode: DependencyNode = root;
291
-
292
- const accessType = dep.cond
293
- ? PropertyAccessType.ConditionalAccess
294
- : PropertyAccessType.UnconditionalAccess;
295
- const depType = dep.cond
296
- ? PropertyAccessType.ConditionalDependency
297
- : PropertyAccessType.UnconditionalDependency;
298
-
299
- for (const property of path) {
300
- // all properties read 'on the way' to a dependency are marked as 'access'
301
- let currChild = currNode.properties.get(property);
302
- if (currChild == null) {
303
- currChild = {
304
- properties: new Map(),
305
- accessType,
306
- };
307
- currNode.properties.set(property, currChild);
308
- } else {
309
- currChild.accessType = merge(currChild.accessType, accessType);
310
- }
311
- currNode = currChild;
312
- }
313
-
314
- // final property read should be marked as `dependency`
315
- currNode.accessType = merge(currNode.accessType, depType);
316
- }
317
-
318
- const results = new Set<ReactiveScopeDependency>();
319
- for (const [root, rootNode] of depRoots.entries()) {
320
- const deps = deriveMinimalDependenciesInSubtree(rootNode);
321
- invariant(
322
- deps.every(
323
- (dep) => dep.accessType === PropertyAccessType.UnconditionalDependency
324
- ),
325
- "[PropagateScopeDependencies] All dependencies must be reduced to unconditional dependencies."
326
- );
327
-
328
- for (const dep of deps) {
329
- results.add({
330
- identifier: root,
331
- path: dep.relativePath,
332
- });
333
- }
334
- }
335
-
336
- return results;
337
-}
338
-
63
class Context {
64
#declarations: DeclMap = new Map();
65
#reassignments: Map<Identifier, Decl> = new Map();
342
- #dependencies: Set<ReactiveScopeDependencyInfo> = new Set();
66
+ #dependencies: ReactiveScopeDependencyTree =
67
+ new ReactiveScopeDependencyTree();
68
#properties: Map<Identifier, ReactiveScopeDependencyInfo> = new Map();
69
#temporaries: Map<Identifier, Place> = new Map();
70
#inConditionalWithinScope: boolean = false;
@@ -354,7 +79,7 @@ class Context {
79
// A nested scope should add all deps it directly uses as its own
80
// unconditional deps, regardless of whether the nested scope is itself
81
// within a conditional
357
- const scopedDependencies = new Set<ReactiveScopeDependencyInfo>();
82
+ const scopedDependencies = new ReactiveScopeDependencyTree();
83
this.#inConditionalWithinScope = false;
84
this.#dependencies = scopedDependencies;
85
this.#scopes.push(scope);
@@ -366,18 +91,21 @@ class Context {
91
this.#dependencies = previousDependencies;
92
this.#inConditionalWithinScope = prevInConditional;
93
369
- const minScopeDependencies = deriveMinimalDependencies(scopedDependencies);
94
+ const minScopeDependencies = scopedDependencies.deriveMinimalDependencies();
95
// propagate dependencies upward using the same rules as normal dependency
96
// collection. child scopes may have dependencies on values created within
97
// the outer scope, which necessarily cannot be dependencies of the outer
98
// scope
99
+ // TODO(@mofeiZ): instead of merging derived minimal dependencies here, we
100
+ // can instead merge the scoped dependency tree. This would let us retain
101
+ // info about unconditional accesses.
102
for (const dep of minScopeDependencies) {
103
this.visitDependency({ ...dep, cond: this.#inConditionalWithinScope });
104
}
105
return minScopeDependencies;
106
}
107
380
- enterConditional(fn: () => void): void {
108
+ enterConditional(fn: () => void) {
109
const prevInConditional = this.#inConditionalWithinScope;
110
this.#inConditionalWithinScope = true;
111
fn();
@@ -509,13 +237,8 @@ class Context {
237
(currentDeclaration.scope == null ||
238
currentDeclaration.scope !== currentScope)
239
) {
512
- // Check if there is an existing dependency that describes this operand
240
+ // Add info about this dependency to the existing tree
241
// We do not try to join/reduce dependencies here due to missing info
514
- for (const dep of this.#dependencies) {
515
- if (areDependenciesEqual(dep, maybeDependency)) {
516
- return;
517
- }
518
- }
242
this.#dependencies.add(maybeDependency);
243
}
244
}