@samitouri / QOS-React-2 / commits / e68ea941e6

[rhir] Promote conditional dependencies accessed by every cfg path

--- Implementation details summarized in comments. Overall, we want to calculate a `ReactiveDependencyTree` for every conditional block. If we know that conditional blocks are exhaustive (e.g. all CFG paths calculates a tree), we can take `intersection(depsFromEachBlock)` and add this to the parent Reactive + conditional scope `parentDeps = union(parentDeps, intersection(...))`. We use trees instead of individual deps here because we can still derive unconditional accesses. e.g. ``` let x = {}; // props.a is an unconditional access here if (foo(other)) { x.a = props.a.b; } else { x.b = props.a.c; } ```

Mofei Zhang committed Mar 6, 2023 at 16:33 UTC e68ea941e6e002411297c31f2b6e51a1795c3994
16 files changed +753 -28
compiler/forget/src/ReactiveScopes/DeriveMinimalDependencies.ts
+97
@@ -91,6 +91,25 @@ export class ReactiveScopeDependencyTree {
91
92 return results;
93 }
94 +
95 + promoteDepsFromExhaustiveConditionals(
96 + trees: Array<ReactiveScopeDependencyTree>
97 + ) {
98 + invariant(
99 + trees.length > 1,
100 + "Expected trees to be at least 2 elements long."
101 + );
102 +
103 + for (const [id, root] of this.#roots) {
104 + const nodesForRootId = mapNonNull(trees, (tree) => tree.#roots.get(id));
105 + if (nodesForRootId) {
106 + addSubtreeIntersection(
107 + nodesForRootId.map((root) => root.properties),
108 + root.properties
109 + );
110 + }
111 + }
112 + }
113 }
114
115 /**
@@ -251,3 +270,81 @@ function deriveMinimalDependenciesInSubtree(
270 }
271 }
272 }
273 +
274 +/**
275 + * Adds intersection(otherProperties) to currProperties, mutating
276 + * currProperties in place. i.e.
277 + * currProperties = union(currProperties, intersection(otherProperties))
278 + *
279 + * Used to merge unconditional accesses from exhaustive conditional branches
280 + * into the parent ReactiveDeps Tree.
281 + * intersection(currProperties) is determined as such:
282 + * - a node is present in the intersection iff it is present in all every
283 + * branch
284 + * - the type of an added node is `UnconditionalDependency` if it is a
285 + * dependency in at least one branch (otherwise `UnconditionalAccess`)
286 + *
287 + * @param otherProperties (read-only) an array of node properties containing
288 + * only unconditionally accessed nodes. Each element represents a
289 + * subtree of reactive dependencies from a single CFG branch.
290 + * otherProperties must represent all reachable branches.
291 + * @param currProperties (mutable) return by argument properties of a node
292 + *
293 + * otherProperties and currProperties must be properties of disjoint nodes
294 + * that represent the same dependency (identifier + path).
295 + */
296 +function addSubtreeIntersection(
297 + otherProperties: Array<Map<string, DependencyNode>>,
298 + currProperties: Map<string, DependencyNode>
299 +) {
300 + invariant(
301 + otherProperties.length > 1,
302 + "[DeriveMinimalDependencies] Expected otherProperties to be at least 2 elements long."
303 + );
304 +
305 + otherProperties.forEach((properties) =>
306 + properties.forEach((node, _) =>
307 + invariant(
308 + !isUnconditional(node.accessType),
309 + "[DeriveMinimalDependencies] Expected otherProperties to only contain unconditional nodes!"
310 + )
311 + )
312 + );
313 +
314 + for (const [propertyName, currNode] of currProperties) {
315 + const otherNodes = mapNonNull(otherProperties, (properties) =>
316 + properties.get(propertyName)
317 + );
318 +
319 + // intersection(otherNodes[propertyName]) only exists if each element in
320 + // otherProperties accesses propertyName.
321 + if (otherNodes) {
322 + addSubtreeIntersection(
323 + otherNodes.map((node) => node.properties),
324 + currNode.properties
325 + );
326 +
327 + const isDep = otherNodes.some((tree) => isDependency(tree.accessType));
328 + const externalAccessType = isDep
329 + ? PropertyAccessType.UnconditionalDependency
330 + : PropertyAccessType.UnconditionalAccess;
331 + currNode.accessType = merge(externalAccessType, currNode.accessType);
332 + }
333 + }
334 +}
335 +
336 +function mapNonNull<T extends NonNullable<V>, V, U>(
337 + arr: Array<U>,
338 + fn: (arg0: U) => T | undefined | null
339 +): Array<T> | null {
340 + const result = [];
341 + for (let i = 0; i < arr.length; i++) {
342 + const element = fn(arr[i]);
343 + if (element) {
344 + result.push(element);
345 + } else {
346 + return null;
347 + }
348 + }
349 + return result;
350 +}
compiler/forget/src/ReactiveScopes/PropagateScopeDependencies.ts
+87 -28
@@ -63,11 +63,18 @@ type Scopes = Array<ReactiveScope>;
63 class Context {
64 #declarations: DeclMap = new Map();
65 #reassignments: Map<Identifier, Decl> = new Map();
66 + // Reactive dependencies used in the current reactive scope.
67 #dependencies: ReactiveScopeDependencyTree =
68 new ReactiveScopeDependencyTree();
69 #properties: Map<Identifier, ReactiveScopeDependencyInfo> = new Map();
70 #temporaries: Map<Identifier, Place> = new Map();
71 #inConditionalWithinScope: boolean = false;
72 + // Reactive dependencies used unconditionally in the current conditional.
73 + // Composed of dependencies:
74 + // - directly accessed within block (added in visitDep)
75 + // - accessed by all cfg branches (added through promoteDeps)
76 + #depsInCurrentConditional: ReactiveScopeDependencyTree =
77 + new ReactiveScopeDependencyTree();
78 #scopes: Scopes = [];
79
80 enter(scope: ReactiveScope, fn: () => void): Set<ReactiveScopeDependency> {
@@ -105,11 +112,48 @@ class Context {
112 return minScopeDependencies;
113 }
114
108 - enterConditional(fn: () => void) {
115 + /**
116 + * We track and return unconditional accesses / deps within this conditional.
117 + * If an object property is always used (i.e. in every conditional path), we
118 + * want to promote it to an unconditional access / dependency.
119 + *
120 + * The caller of `enterConditional` is responsible determining for promotion.
121 + * i.e. call promoteDepsFromExhaustiveConditionals to merge returned results.
122 + *
123 + * e.g. we want to mark props.a.b as an unconditional dep here
124 + * if (foo(...)) {
125 + * access(props.a.b);
126 + * } else {
127 + * access(props.a.b);
128 + * }
129 + */
130 + enterConditional(fn: () => void): ReactiveScopeDependencyTree {
131 const prevInConditional = this.#inConditionalWithinScope;
132 + const prevUncondAccessed = this.#depsInCurrentConditional;
133 this.#inConditionalWithinScope = true;
134 + this.#depsInCurrentConditional = new ReactiveScopeDependencyTree();
135 fn();
136 + const result = this.#depsInCurrentConditional;
137 this.#inConditionalWithinScope = prevInConditional;
138 + this.#depsInCurrentConditional = prevUncondAccessed;
139 + return result;
140 + }
141 +
142 + /**
143 + * Add dependencies from exhaustive CFG paths into the current ReactiveDeps
144 + * tree. If a property is used in every CFG path, it is promoted to an
145 + * unconditional access / dependency here.
146 + * @param depsInConditionals
147 + */
148 + promoteDepsFromExhaustiveConditionals(
149 + depsInConditionals: Array<ReactiveScopeDependencyTree>
150 + ) {
151 + this.#dependencies.promoteDepsFromExhaustiveConditionals(
152 + depsInConditionals
153 + );
154 + this.#depsInCurrentConditional.promoteDepsFromExhaustiveConditionals(
155 + depsInConditionals
156 + );
157 }
158
159 /**
@@ -237,6 +281,10 @@ class Context {
281 (currentDeclaration.scope == null ||
282 currentDeclaration.scope !== currentScope)
283 ) {
284 + this.#depsInCurrentConditional.add({
285 + ...maybeDependency,
286 + cond: true,
287 + });
288 // Add info about this dependency to the existing tree
289 // We do not try to join/reduce dependencies here due to missing info
290 this.#dependencies.add(maybeDependency);
@@ -316,38 +364,43 @@ function visit(context: Context, block: ReactiveBlock): void {
364 }
365 case "if": {
366 context.visitOperand(terminal.test);
319 - /**
320 - * TODO: Track dependencies always accessed within consequent and ones
321 - * always accessed within alternate. If a dependency is always accessed in
322 - * both, we can promote it to an unconditional dependency.
323 - *
324 - * e.g. props.a.b is unconditionally accessed here.
325 - * if (foo(...)) {
326 - * access(props.a.b);
327 - * } else {
328 - * access(props.a.b);
329 - * }
330 - *
331 - * To deal with nested if-branches, enterConditional should return a list
332 - * of dependencies unconditionally accessed within the callback.
333 - */
334 - context.enterConditional(() => {
335 - visit(context, terminal.consequent);
336 - if (terminal.alternate !== null) {
337 - visit(context, terminal.alternate);
338 - }
367 + const { consequent, alternate } = terminal;
368 + const depsInIf = context.enterConditional(() => {
369 + visit(context, consequent);
370 });
371 + if (alternate !== null) {
372 + const depsInElse = context.enterConditional(() => {
373 + visit(context, alternate);
374 + });
375 + context.promoteDepsFromExhaustiveConditionals([
376 + depsInIf,
377 + depsInElse,
378 + ]);
379 + }
380 break;
381 }
382 case "switch": {
383 context.visitOperand(terminal.test);
344 - context.enterConditional(() => {
345 - for (const case_ of terminal.cases) {
346 - if (case_.block !== undefined) {
347 - visit(context, case_.block);
348 - }
384 + const depsInCases = [];
385 + let foundDefault = false;
386 + // This can underestimate unconditional accesses due to the current
387 + // CFG representation for fallthrough. This is safe. It only
388 + // reduces granularity of dependencies.
389 + for (const { test, block } of terminal.cases) {
390 + if (test == null) {
391 + foundDefault = true;
392 }
350 - });
393 + if (block !== undefined) {
394 + depsInCases.push(
395 + context.enterConditional(() => {
396 + visit(context, block);
397 + })
398 + );
399 + }
400 + }
401 + if (foundDefault) {
402 + context.promoteDepsFromExhaustiveConditionals(depsInCases);
403 + }
404 break;
405 }
406 default: {
@@ -382,10 +435,16 @@ function visitReactiveValue(
435 case "ConditionalExpression": {
436 visitReactiveValue(context, id, value.test);
437
385 - context.enterConditional(() => {
438 + const consequentDeps = context.enterConditional(() => {
439 visitReactiveValue(context, id, value.consequent);
440 + });
441 + const alternateDeps = context.enterConditional(() => {
442 visitReactiveValue(context, id, value.alternate);
443 });
444 + context.promoteDepsFromExhaustiveConditionals([
445 + consequentDeps,
446 + alternateDeps,
447 + ]);
448 break;
449 }
450 case "SequenceExpression": {
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-condexpr.expect.md new
+41
@@ -0,0 +1,41 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should be added as a unconditional dependency to the reactive
6 +// scope that produces x, since it is accessed unconditionally in all cfg
7 +// paths
8 +
9 +function TestCondDepInConditionalExpr(props, other) {
10 + const x = foo(other) ? bar(props.a.b) : baz(props.a.b);
11 + return x;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +// props.a.b should be added as a unconditional dependency to the reactive
20 +// scope that produces x, since it is accessed unconditionally in all cfg
21 +// paths
22 +
23 +function TestCondDepInConditionalExpr(props, other) {
24 + const $ = React.unstable_useMemoCache(3);
25 + const c_0 = $[0] !== other;
26 + const c_1 = $[1] !== props.a.b;
27 + let t0;
28 + if (c_0 || c_1) {
29 + t0 = foo(other) ? bar(props.a.b) : baz(props.a.b);
30 + $[0] = other;
31 + $[1] = props.a.b;
32 + $[2] = t0;
33 + } else {
34 + t0 = $[2];
35 + }
36 + const x = t0;
37 + return x;
38 +}
39 +
40 +```
41 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-condexpr.js new
+8
@@ -0,0 +1,8 @@
1 +// props.a.b should be added as a unconditional dependency to the reactive
2 +// scope that produces x, since it is accessed unconditionally in all cfg
3 +// paths
4 +
5 +function TestCondDepInConditionalExpr(props, other) {
6 + const x = foo(other) ? bar(props.a.b) : baz(props.a.b);
7 + return x;
8 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-ifelse.expect.md new
+59
@@ -0,0 +1,59 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should be added as a unconditional dependency to the reactive
6 +// scope that produces x, since it is accessed unconditionally in all cfg
7 +// paths
8 +
9 +function TestCondDepInDirectIfElse(props, other) {
10 + const x = {};
11 + if (foo(other)) {
12 + x.b = props.a.b;
13 + } else {
14 + x.c = props.a.b;
15 + }
16 + return x;
17 +}
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +// props.a.b should be added as a unconditional dependency to the reactive
25 +// scope that produces x, since it is accessed unconditionally in all cfg
26 +// paths
27 +
28 +function TestCondDepInDirectIfElse(props, other) {
29 + const $ = React.unstable_useMemoCache(5);
30 + const c_0 = $[0] !== other;
31 + const c_1 = $[1] !== props.a.b;
32 + let x;
33 + if (c_0 || c_1) {
34 + x = {};
35 + const c_3 = $[3] !== other;
36 + let t0;
37 + if (c_3) {
38 + t0 = foo(other);
39 + $[3] = other;
40 + $[4] = t0;
41 + } else {
42 + t0 = $[4];
43 + }
44 + if (t0) {
45 + x.b = props.a.b;
46 + } else {
47 + x.c = props.a.b;
48 + }
49 + $[0] = other;
50 + $[1] = props.a.b;
51 + $[2] = x;
52 + } else {
53 + x = $[2];
54 + }
55 + return x;
56 +}
57 +
58 +```
59 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-ifelse.js new
+13
@@ -0,0 +1,13 @@
1 +// props.a.b should be added as a unconditional dependency to the reactive
2 +// scope that produces x, since it is accessed unconditionally in all cfg
3 +// paths
4 +
5 +function TestCondDepInDirectIfElse(props, other) {
6 + const x = {};
7 + if (foo(other)) {
8 + x.b = props.a.b;
9 + } else {
10 + x.c = props.a.b;
11 + }
12 + return x;
13 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-nested-ifelse-missing.expect.md new
+68
@@ -0,0 +1,68 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should NOT be added as a unconditional dependency to the reactive
6 +// scope that produces x if it is not accessed in every path
7 +
8 +function TestCondDepInNestedIfElse(props, other) {
9 + const x = {};
10 + if (foo(other)) {
11 + if (bar()) {
12 + x.a = props.a.b;
13 + }
14 + } else {
15 + x.d = props.a.b;
16 + }
17 + return x;
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +// props.a.b should NOT be added as a unconditional dependency to the reactive
26 +// scope that produces x if it is not accessed in every path
27 +
28 +function TestCondDepInNestedIfElse(props, other) {
29 + const $ = React.unstable_useMemoCache(6);
30 + const c_0 = $[0] !== other;
31 + const c_1 = $[1] !== props;
32 + let x;
33 + if (c_0 || c_1) {
34 + x = {};
35 + const c_3 = $[3] !== other;
36 + let t0;
37 + if (c_3) {
38 + t0 = foo(other);
39 + $[3] = other;
40 + $[4] = t0;
41 + } else {
42 + t0 = $[4];
43 + }
44 + if (t0) {
45 + let t1;
46 + if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
47 + t1 = bar();
48 + $[5] = t1;
49 + } else {
50 + t1 = $[5];
51 + }
52 + if (t1) {
53 + x.a = props.a.b;
54 + }
55 + } else {
56 + x.d = props.a.b;
57 + }
58 + $[0] = other;
59 + $[1] = props;
60 + $[2] = x;
61 + } else {
62 + x = $[2];
63 + }
64 + return x;
65 +}
66 +
67 +```
68 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-nested-ifelse-missing.js new
+14
@@ -0,0 +1,14 @@
1 +// props.a.b should NOT be added as a unconditional dependency to the reactive
2 +// scope that produces x if it is not accessed in every path
3 +
4 +function TestCondDepInNestedIfElse(props, other) {
5 + const x = {};
6 + if (foo(other)) {
7 + if (bar()) {
8 + x.a = props.a.b;
9 + }
10 + } else {
11 + x.d = props.a.b;
12 + }
13 + return x;
14 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-nested-ifelse.expect.md new
+89
@@ -0,0 +1,89 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should be added as a unconditional dependency to the reactive
6 +// scope that produces x, since it is accessed unconditionally in all cfg
7 +// paths
8 +
9 +function TestCondDepInNestedIfElse(props, other) {
10 + const x = {};
11 + if (foo(other)) {
12 + if (bar()) {
13 + x.a = props.a.b;
14 + } else {
15 + x.b = props.a.b;
16 + }
17 + } else if (baz(other)) {
18 + x.c = props.a.b;
19 + } else {
20 + x.d = props.a.b;
21 + }
22 + return x;
23 +}
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// props.a.b should be added as a unconditional dependency to the reactive
31 +// scope that produces x, since it is accessed unconditionally in all cfg
32 +// paths
33 +
34 +function TestCondDepInNestedIfElse(props, other) {
35 + const $ = React.unstable_useMemoCache(8);
36 + const c_0 = $[0] !== other;
37 + const c_1 = $[1] !== props.a.b;
38 + let x;
39 + if (c_0 || c_1) {
40 + x = {};
41 + const c_3 = $[3] !== other;
42 + let t0;
43 + if (c_3) {
44 + t0 = foo(other);
45 + $[3] = other;
46 + $[4] = t0;
47 + } else {
48 + t0 = $[4];
49 + }
50 + if (t0) {
51 + let t1;
52 + if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
53 + t1 = bar();
54 + $[5] = t1;
55 + } else {
56 + t1 = $[5];
57 + }
58 + if (t1) {
59 + x.a = props.a.b;
60 + } else {
61 + x.b = props.a.b;
62 + }
63 + } else {
64 + const c_6 = $[6] !== other;
65 + let t2;
66 + if (c_6) {
67 + t2 = baz(other);
68 + $[6] = other;
69 + $[7] = t2;
70 + } else {
71 + t2 = $[7];
72 + }
73 + if (t2) {
74 + x.c = props.a.b;
75 + } else {
76 + x.d = props.a.b;
77 + }
78 + }
79 + $[0] = other;
80 + $[1] = props.a.b;
81 + $[2] = x;
82 + } else {
83 + x = $[2];
84 + }
85 + return x;
86 +}
87 +
88 +```
89 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-nested-ifelse.js new
+19
@@ -0,0 +1,19 @@
1 +// props.a.b should be added as a unconditional dependency to the reactive
2 +// scope that produces x, since it is accessed unconditionally in all cfg
3 +// paths
4 +
5 +function TestCondDepInNestedIfElse(props, other) {
6 + const x = {};
7 + if (foo(other)) {
8 + if (bar()) {
9 + x.a = props.a.b;
10 + } else {
11 + x.b = props.a.b;
12 + }
13 + } else if (baz(other)) {
14 + x.c = props.a.b;
15 + } else {
16 + x.d = props.a.b;
17 + }
18 + return x;
19 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch-missing-case.expect.md new
+71
@@ -0,0 +1,71 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should NOT be added as a unconditional dependency to the reactive
6 +// scope that produces x if it is not accessed in every path
7 +
8 +function TestCondDepInSwitchMissingCase(props, other) {
9 + const x = {};
10 + switch (foo(other)) {
11 + case 1:
12 + x.a = props.a.b;
13 + break;
14 + case 2:
15 + x.b = 42;
16 + break;
17 + default:
18 + x.c = props.a.b;
19 + break;
20 + }
21 + return x;
22 +}
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +// props.a.b should NOT be added as a unconditional dependency to the reactive
30 +// scope that produces x if it is not accessed in every path
31 +
32 +function TestCondDepInSwitchMissingCase(props, other) {
33 + const $ = React.unstable_useMemoCache(5);
34 + const c_0 = $[0] !== other;
35 + const c_1 = $[1] !== props;
36 + let x;
37 + if (c_0 || c_1) {
38 + x = {};
39 + const c_3 = $[3] !== other;
40 + let t0;
41 + if (c_3) {
42 + t0 = foo(other);
43 + $[3] = other;
44 + $[4] = t0;
45 + } else {
46 + t0 = $[4];
47 + }
48 + bb1: switch (t0) {
49 + case 1: {
50 + x.a = props.a.b;
51 + break bb1;
52 + }
53 + case 2: {
54 + x.b = 42;
55 + break bb1;
56 + }
57 + default: {
58 + x.c = props.a.b;
59 + }
60 + }
61 + $[0] = other;
62 + $[1] = props;
63 + $[2] = x;
64 + } else {
65 + x = $[2];
66 + }
67 + return x;
68 +}
69 +
70 +```
71 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch-missing-case.js new
+18
@@ -0,0 +1,18 @@
1 +// props.a.b should NOT be added as a unconditional dependency to the reactive
2 +// scope that produces x if it is not accessed in every path
3 +
4 +function TestCondDepInSwitchMissingCase(props, other) {
5 + const x = {};
6 + switch (foo(other)) {
7 + case 1:
8 + x.a = props.a.b;
9 + break;
10 + case 2:
11 + x.b = 42;
12 + break;
13 + default:
14 + x.c = props.a.b;
15 + break;
16 + }
17 + return x;
18 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch-missing-default.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should NOT be added as a unconditional dependency to the reactive
6 +// scope that produces x if it is not accessed in the default case.
7 +
8 +function TestCondDepInSwitchMissingDefault(props, other) {
9 + const x = {};
10 + switch (foo(other)) {
11 + case 1:
12 + x.a = props.a.b;
13 + break;
14 + case 2:
15 + x.b = props.a.b;
16 + break;
17 + }
18 + return x;
19 +}
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +// props.a.b should NOT be added as a unconditional dependency to the reactive
27 +// scope that produces x if it is not accessed in the default case.
28 +
29 +function TestCondDepInSwitchMissingDefault(props, other) {
30 + const $ = React.unstable_useMemoCache(5);
31 + const c_0 = $[0] !== other;
32 + const c_1 = $[1] !== props;
33 + let x;
34 + if (c_0 || c_1) {
35 + x = {};
36 + const c_3 = $[3] !== other;
37 + let t0;
38 + if (c_3) {
39 + t0 = foo(other);
40 + $[3] = other;
41 + $[4] = t0;
42 + } else {
43 + t0 = $[4];
44 + }
45 + bb1: switch (t0) {
46 + case 1: {
47 + x.a = props.a.b;
48 + break bb1;
49 + }
50 + case 2: {
51 + x.b = props.a.b;
52 + }
53 + }
54 + $[0] = other;
55 + $[1] = props;
56 + $[2] = x;
57 + } else {
58 + x = $[2];
59 + }
60 + return x;
61 +}
62 +
63 +```
64 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch-missing-default.js new
+15
@@ -0,0 +1,15 @@
1 +// props.a.b should NOT be added as a unconditional dependency to the reactive
2 +// scope that produces x if it is not accessed in the default case.
3 +
4 +function TestCondDepInSwitchMissingDefault(props, other) {
5 + const x = {};
6 + switch (foo(other)) {
7 + case 1:
8 + x.a = props.a.b;
9 + break;
10 + case 2:
11 + x.b = props.a.b;
12 + break;
13 + }
14 + return x;
15 +}
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch.expect.md new
+72
@@ -0,0 +1,72 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// props.a.b should be added as a unconditional dependency to the reactive
6 +// scope that produces x, since it is accessed unconditionally in all cfg
7 +// paths
8 +
9 +function TestCondDepInSwitch(props, other) {
10 + const x = {};
11 + switch (foo(other)) {
12 + case 1:
13 + x.a = props.a.b;
14 + break;
15 + case 2:
16 + x.b = props.a.b;
17 + break;
18 + default:
19 + x.c = props.a.b;
20 + }
21 + return x;
22 +}
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +// props.a.b should be added as a unconditional dependency to the reactive
30 +// scope that produces x, since it is accessed unconditionally in all cfg
31 +// paths
32 +
33 +function TestCondDepInSwitch(props, other) {
34 + const $ = React.unstable_useMemoCache(5);
35 + const c_0 = $[0] !== other;
36 + const c_1 = $[1] !== props.a.b;
37 + let x;
38 + if (c_0 || c_1) {
39 + x = {};
40 + const c_3 = $[3] !== other;
41 + let t0;
42 + if (c_3) {
43 + t0 = foo(other);
44 + $[3] = other;
45 + $[4] = t0;
46 + } else {
47 + t0 = $[4];
48 + }
49 + bb1: switch (t0) {
50 + case 1: {
51 + x.a = props.a.b;
52 + break bb1;
53 + }
54 + case 2: {
55 + x.b = props.a.b;
56 + break bb1;
57 + }
58 + default: {
59 + x.c = props.a.b;
60 + }
61 + }
62 + $[0] = other;
63 + $[1] = props.a.b;
64 + $[2] = x;
65 + } else {
66 + x = $[2];
67 + }
68 + return x;
69 +}
70 +
71 +```
72 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/reduce-reactive-cond-deps-cfg-switch.js new
+18
@@ -0,0 +1,18 @@
1 +// props.a.b should be added as a unconditional dependency to the reactive
2 +// scope that produces x, since it is accessed unconditionally in all cfg
3 +// paths
4 +
5 +function TestCondDepInSwitch(props, other) {
6 + const x = {};
7 + switch (foo(other)) {
8 + case 1:
9 + x.a = props.a.b;
10 + break;
11 + case 2:
12 + x.b = props.a.b;
13 + break;
14 + default:
15 + x.c = props.a.b;
16 + }
17 + return x;
18 +}