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 {
9
+ ReactiveFunctionTransform,
10
+ Transformed,
11
+ visitReactiveFunction,
12
+} from ".";
13
+import {
14
+ IdentifierId,
15
+ ReactiveFunction,
16
+ ReactiveInstruction,
17
+ ReactiveScopeBlock,
18
+ ReactiveStatement,
19
+} from "../HIR";
20
+
21
+/**
22
+ * Some instructions will *always* produce a new value, and unless memoized will *always*
23
+ * invalidate downstream reactive scopes. This pass finds such values and prunes downstream
24
+ * memoization.
25
+ *
26
+ * NOTE: function calls are an edge-case: function calls *may* return primitives, so this
27
+ * pass optimistically assumes they do. Therefore, unmemoized function calls will *not*
28
+ * prune downstream memoization. Only guaranteed new allocations, such as object and array
29
+ * literals, will cause pruning.
30
+ */
31
+export function pruneAlwaysInvalidatingScopes(fn: ReactiveFunction): void {
32
+ visitReactiveFunction(fn, new Transform(), false);
33
+}
34
+
35
+class Transform extends ReactiveFunctionTransform<boolean> {
36
+ alwaysInvalidatingValues: Set<IdentifierId> = new Set();
37
+ unmemoizedValues: Set<IdentifierId> = new Set();
38
+
39
+ override transformInstruction(
40
+ instruction: ReactiveInstruction,
41
+ withinScope: boolean
42
+ ): Transformed<ReactiveStatement> {
43
+ this.visitInstruction(instruction, withinScope);
44
+
45
+ const { lvalue, value } = instruction;
46
+ switch (value.kind) {
47
+ case "ArrayExpression":
48
+ case "ObjectExpression":
49
+ case "JsxExpression":
50
+ case "JsxFragment":
51
+ case "NewExpression": {
52
+ if (lvalue !== null) {
53
+ this.alwaysInvalidatingValues.add(lvalue.identifier.id);
54
+ if (!withinScope) {
55
+ this.unmemoizedValues.add(lvalue.identifier.id);
56
+ }
57
+ }
58
+ break;
59
+ }
60
+ case "StoreLocal": {
61
+ if (this.alwaysInvalidatingValues.has(value.value.identifier.id)) {
62
+ this.alwaysInvalidatingValues.add(value.lvalue.place.identifier.id);
63
+ }
64
+ if (this.unmemoizedValues.has(value.value.identifier.id)) {
65
+ this.unmemoizedValues.add(value.lvalue.place.identifier.id);
66
+ }
67
+ break;
68
+ }
69
+ case "LoadLocal": {
70
+ if (
71
+ lvalue !== null &&
72
+ this.alwaysInvalidatingValues.has(value.place.identifier.id)
73
+ ) {
74
+ this.alwaysInvalidatingValues.add(lvalue.identifier.id);
75
+ }
76
+ if (
77
+ lvalue !== null &&
78
+ this.unmemoizedValues.has(value.place.identifier.id)
79
+ ) {
80
+ this.unmemoizedValues.add(lvalue.identifier.id);
81
+ }
82
+ break;
83
+ }
84
+ }
85
+ return { kind: "keep" };
86
+ }
87
+
88
+ override transformScope(
89
+ scopeBlock: ReactiveScopeBlock,
90
+ _withinScope: boolean
91
+ ): Transformed<ReactiveStatement> {
92
+ this.visitScope(scopeBlock, true);
93
+
94
+ for (const dep of scopeBlock.scope.dependencies) {
95
+ if (this.unmemoizedValues.has(dep.identifier.id)) {
96
+ /*
97
+ * This scope depends on an always-invalidating value so the scope will always invalidate:
98
+ * prune it to avoid wasted comparisons
99
+ */
100
+ for (const [id, _decl] of scopeBlock.scope.declarations) {
101
+ if (this.alwaysInvalidatingValues.has(id)) {
102
+ this.unmemoizedValues.add(id);
103
+ }
104
+ }
105
+ for (const identifier of scopeBlock.scope.reassignments) {
106
+ if (this.alwaysInvalidatingValues.has(identifier.id)) {
107
+ this.unmemoizedValues.add(identifier.id);
108
+ }
109
+ }
110
+ return { kind: "replace-many", value: scopeBlock.instructions };
111
+ }
112
+ }
113
+ return { kind: "keep" };
114
+ }
115
+}