@samitouri / QOS-React-2 / commits / 25bfe728aa

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