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 { CompilerError } from "..";
9
+import {
10
+ Effect,
11
+ HIRFunction,
12
+ Identifier,
13
+ IdentifierId,
14
+ Place,
15
+ getHookKind,
16
+} from "../HIR";
17
+import {
18
+ eachInstructionLValue,
19
+ eachInstructionValueOperand,
20
+ eachTerminalOperand,
21
+} from "../HIR/visitors";
22
+import { hasBackEdge } from "../Optimization/DeadCodeElimination";
23
+import { assertExhaustive } from "../Utils/utils";
24
+
25
+/**
26
+ * Infers which `Place`s are reactive, ie may *semantically* change
27
+ * over the course of the component/hook's lifetime. Places are reactive
28
+ * if they derive from source source of reactivity, which includes the
29
+ * following categories.
30
+ *
31
+ * ## Props
32
+ *
33
+ * Props may change so they're reactive:
34
+ *
35
+ * ## Hooks
36
+ *
37
+ * Hooks may access state or context, which can change so they're reactive.
38
+ *
39
+ * ## Mutation with reactive operands
40
+ *
41
+ * Any value that is mutated in an instruction that also has reactive operands
42
+ * could cause the modified value to capture a reference to the reactive value,
43
+ * making the mutated value reactive.
44
+ *
45
+ * Ex:
46
+ * ```
47
+ * function Component(props) {
48
+ * const x = {}; // not yet reactive
49
+ * x.y = props.y;
50
+ * }
51
+ * ```
52
+ *
53
+ * Here `x` is modified in an instruction that has a reactive operand (`props.y`)
54
+ * so x becomes reactive.
55
+ *
56
+ * ## Conditional assignment based on a reactive condition
57
+ *
58
+ * Conditionally reassigning a variable based on a condition which is reactive means
59
+ * that the value being assigned could change, hence that variable also becomes
60
+ * reactive.
61
+ *
62
+ * ```
63
+ * function Component(props) {
64
+ * let x;
65
+ * if (props.cond) {
66
+ * x = 1;
67
+ * } else {
68
+ * x = 2;
69
+ * }
70
+ * return x;
71
+ * }
72
+ * ```
73
+ *
74
+ * Here `x` is never assigned a reactive value (it is assigned the constant 1 or 2) but
75
+ * the condition, `props.cond`, is reactive, and therefore `x` could change reactively too.
76
+ *
77
+ *
78
+ * # Algorithm
79
+ *
80
+ * The algorithm uses a fixpoint iteration in order to propagate reactivity "forward" through
81
+ * the control-flow graph. We track whether each IdentifierId is reactive and terminate when
82
+ * there are no changes after a given pass over the CFG.
83
+ */
84
+export function inferReactivePlaces(fn: HIRFunction): void {
85
+ const reactiveIdentifiers = new ReactivityMap();
86
+ for (const param of fn.params) {
87
+ const place = param.kind === "Identifier" ? param : param.place;
88
+ reactiveIdentifiers.markReactive(place);
89
+ }
90
+
91
+ const hasLoop = hasBackEdge(fn);
92
+ do {
93
+ for (const [, block] of fn.body.blocks) {
94
+ for (const phi of block.phis) {
95
+ for (const [, operand] of phi.operands) {
96
+ if (reactiveIdentifiers.isReactiveIdentifier(operand)) {
97
+ reactiveIdentifiers.markReactiveIdentifier(phi.id);
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ for (const instruction of block.instructions) {
103
+ const { value } = instruction;
104
+ let hasReactiveInput = false;
105
+ // NOTE: we want to mark all operands as reactive or not, so we
106
+ // avoid short-circuting here
107
+ for (const operand of eachInstructionValueOperand(value)) {
108
+ const reactive = reactiveIdentifiers.isReactive(operand);
109
+ hasReactiveInput ||= reactive;
110
+ }
111
+
112
+ // Hooks may always return a reactive variable, even if their inputs are
113
+ // non-reactive, because they can access state or context.
114
+ if (
115
+ value.kind === "CallExpression" &&
116
+ getHookKind(fn.env, value.callee.identifier) != null
117
+ ) {
118
+ hasReactiveInput = true;
119
+ } else if (
120
+ value.kind === "MethodCall" &&
121
+ getHookKind(fn.env, value.property.identifier) != null
122
+ ) {
123
+ hasReactiveInput = true;
124
+ }
125
+
126
+ if (hasReactiveInput) {
127
+ for (const lvalue of eachInstructionLValue(instruction)) {
128
+ reactiveIdentifiers.markReactive(lvalue);
129
+ }
130
+
131
+ for (const operand of eachInstructionValueOperand(value)) {
132
+ switch (operand.effect) {
133
+ case Effect.Capture:
134
+ case Effect.Store:
135
+ case Effect.ConditionallyMutate:
136
+ case Effect.Mutate: {
137
+ reactiveIdentifiers.markReactive(operand);
138
+ break;
139
+ }
140
+ case Effect.Freeze:
141
+ case Effect.Read: {
142
+ // no-op
143
+ break;
144
+ }
145
+ case Effect.Unknown: {
146
+ CompilerError.invariant(false, {
147
+ reason: "Unexpected unknown effect",
148
+ description: null,
149
+ loc: operand.loc,
150
+ suggestions: null,
151
+ });
152
+ }
153
+ default: {
154
+ assertExhaustive(
155
+ operand.effect,
156
+ `Unexpected effect kind '${operand.effect}'`
157
+ );
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ for (const operand of eachTerminalOperand(block.terminal)) {
164
+ reactiveIdentifiers.isReactive(operand);
165
+ }
166
+ }
167
+ } while (reactiveIdentifiers.snapshot() && hasLoop);
168
+}
169
+
170
+class ReactivityMap {
171
+ hasChanges: boolean = false;
172
+ reactive: Set<IdentifierId> = new Set();
173
+
174
+ isReactive(place: Place): boolean {
175
+ const reactive = this.reactive.has(place.identifier.id);
176
+ if (reactive) {
177
+ place.reactive = true;
178
+ }
179
+ return reactive;
180
+ }
181
+
182
+ isReactiveIdentifier(identifier: Identifier): boolean {
183
+ return this.reactive.has(identifier.id);
184
+ }
185
+
186
+ markReactive(place: Place): void {
187
+ place.reactive = true;
188
+ this.markReactiveIdentifier(place.identifier);
189
+ }
190
+
191
+ markReactiveIdentifier(identifier: Identifier): void {
192
+ if (!this.reactive.has(identifier.id)) {
193
+ this.hasChanges = true;
194
+ this.reactive.add(identifier.id);
195
+ }
196
+ }
197
+
198
+ snapshot(): boolean {
199
+ const hasChanges = this.hasChanges;
200
+ this.hasChanges = false;
201
+ return hasChanges;
202
+ }
203
+}