main
ts 413 lines 12.8 KB
Raw
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 Environment,
12 HIRFunction,
13 Identifier,
14 IdentifierId,
15 Instruction,
16 Place,
17 evaluatesToStableTypeOrContainer,
18 getHookKind,
19 isStableType,
20 isStableTypeContainer,
21 isUseOperator,
22 } from '../HIR';
23 import {
24 eachInstructionLValue,
25 eachInstructionOperand,
26 eachInstructionValueOperand,
27 eachTerminalOperand,
28 } from '../HIR/visitors';
29 import {
30 findDisjointMutableValues,
31 isMutable,
32 } from '../ReactiveScopes/InferReactiveScopeVariables';
33 import DisjointSet from '../Utils/DisjointSet';
34 import {assertExhaustive} from '../Utils/utils';
35 import {createControlDominators} from './ControlDominators';
36
37 /**
38 * Side map to track and propagate sources of stability (i.e. hook calls such as
39 * `useRef()` and property reads such as `useState()[1]). Note that this
40 * requires forward data flow analysis since stability is not part of React
41 * Compiler's type system.
42 */
43 class StableSidemap {
44 map: Map<IdentifierId, {isStable: boolean}> = new Map();
45 env: Environment;
46
47 constructor(env: Environment) {
48 this.env = env;
49 }
50
51 handleInstruction(instr: Instruction): void {
52 const {value, lvalue} = instr;
53
54 switch (value.kind) {
55 case 'CallExpression':
56 case 'MethodCall': {
57 /**
58 * Sources of stability are known hook calls
59 */
60 if (evaluatesToStableTypeOrContainer(this.env, instr)) {
61 if (isStableType(lvalue.identifier)) {
62 this.map.set(lvalue.identifier.id, {
63 isStable: true,
64 });
65 } else {
66 this.map.set(lvalue.identifier.id, {
67 isStable: false,
68 });
69 }
70 }
71 break;
72 }
73
74 case 'Destructure':
75 case 'PropertyLoad': {
76 /**
77 * PropertyLoads may from stable containers may also produce stable
78 * values. ComputedLoads are technically safe for now (as all stable
79 * containers have differently-typed elements), but are not handled as
80 * they should be rare anyways.
81 */
82 const source =
83 value.kind === 'Destructure'
84 ? value.value.identifier.id
85 : value.object.identifier.id;
86 const entry = this.map.get(source);
87 if (entry) {
88 for (const lvalue of eachInstructionLValue(instr)) {
89 if (isStableTypeContainer(lvalue.identifier)) {
90 this.map.set(lvalue.identifier.id, {
91 isStable: false,
92 });
93 } else if (isStableType(lvalue.identifier)) {
94 this.map.set(lvalue.identifier.id, {
95 isStable: true,
96 });
97 }
98 }
99 }
100 break;
101 }
102
103 case 'StoreLocal': {
104 const entry = this.map.get(value.value.identifier.id);
105 if (entry) {
106 this.map.set(lvalue.identifier.id, entry);
107 this.map.set(value.lvalue.place.identifier.id, entry);
108 }
109 break;
110 }
111
112 case 'LoadLocal': {
113 const entry = this.map.get(value.place.identifier.id);
114 if (entry) {
115 this.map.set(lvalue.identifier.id, entry);
116 }
117 break;
118 }
119 }
120 }
121
122 isStable(id: IdentifierId): boolean {
123 const entry = this.map.get(id);
124 return entry != null ? entry.isStable : false;
125 }
126 }
127 /*
128 * Infers which `Place`s are reactive, ie may *semantically* change
129 * over the course of the component/hook's lifetime. Places are reactive
130 * if they derive from source source of reactivity, which includes the
131 * following categories.
132 *
133 * ## Props
134 *
135 * Props may change so they're reactive:
136 *
137 * ## Hooks
138 *
139 * Hooks may access state or context, which can change so they're reactive.
140 *
141 * ## Mutation with reactive operands
142 *
143 * Any value that is mutated in an instruction that also has reactive operands
144 * could cause the modified value to capture a reference to the reactive value,
145 * making the mutated value reactive.
146 *
147 * Ex:
148 * ```
149 * function Component(props) {
150 * const x = {}; // not yet reactive
151 * x.y = props.y;
152 * }
153 * ```
154 *
155 * Here `x` is modified in an instruction that has a reactive operand (`props.y`)
156 * so x becomes reactive.
157 *
158 * ## Conditional assignment based on a reactive condition
159 *
160 * Conditionally reassigning a variable based on a condition which is reactive means
161 * that the value being assigned could change, hence that variable also becomes
162 * reactive.
163 *
164 * ```
165 * function Component(props) {
166 * let x;
167 * if (props.cond) {
168 * x = 1;
169 * } else {
170 * x = 2;
171 * }
172 * return x;
173 * }
174 * ```
175 *
176 * Here `x` is never assigned a reactive value (it is assigned the constant 1 or 2) but
177 * the condition, `props.cond`, is reactive, and therefore `x` could change reactively too.
178 *
179 *
180 * # Algorithm
181 *
182 * The algorithm uses a fixpoint iteration in order to propagate reactivity "forward" through
183 * the control-flow graph. We track whether each IdentifierId is reactive and terminate when
184 * there are no changes after a given pass over the CFG.
185 *
186 * Note that in Forget it's possible to create a "readonly" reference to a value where
187 * the reference is created within that value's mutable range:
188 *
189 * ```javascript
190 * const x = [];
191 * const z = [x];
192 * x.push(props.input);
193 *
194 * return <div>{z}</div>;
195 * ```
196 *
197 * Here `z` is never used to mutate the value, but it is aliasing `x` which
198 * is mutated after the creation of the alias. The pass needs to account for
199 * values which become reactive via mutability, and propagate this reactivity
200 * to these readonly aliases. Using forward data flow is insufficient since
201 * this information needs to propagate "backwards" from the `x.push(props.input)`
202 * to the previous `z = [x]` line. We use a fixpoint iteration even if the
203 * program has no back edges to accomplish this.
204 */
205 export function inferReactivePlaces(fn: HIRFunction): void {
206 const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
207 const stableIdentifierSources = new StableSidemap(fn.env);
208 for (const param of fn.params) {
209 const place = param.kind === 'Identifier' ? param : param.place;
210 reactiveIdentifiers.markReactive(place);
211 }
212
213 const isReactiveControlledBlock = createControlDominators(fn, place =>
214 reactiveIdentifiers.isReactive(place),
215 );
216
217 do {
218 for (const [, block] of fn.body.blocks) {
219 let hasReactiveControl = isReactiveControlledBlock(block.id);
220
221 for (const phi of block.phis) {
222 if (reactiveIdentifiers.isReactive(phi.place)) {
223 // Already marked reactive on a previous pass
224 continue;
225 }
226 let isPhiReactive = false;
227 for (const [, operand] of phi.operands) {
228 if (reactiveIdentifiers.isReactive(operand)) {
229 isPhiReactive = true;
230 break;
231 }
232 }
233 if (isPhiReactive) {
234 reactiveIdentifiers.markReactive(phi.place);
235 } else {
236 for (const [pred] of phi.operands) {
237 if (isReactiveControlledBlock(pred)) {
238 reactiveIdentifiers.markReactive(phi.place);
239 break;
240 }
241 }
242 }
243 }
244 for (const instruction of block.instructions) {
245 stableIdentifierSources.handleInstruction(instruction);
246 const {value} = instruction;
247 let hasReactiveInput = false;
248 /*
249 * NOTE: we want to mark all operands as reactive or not, so we
250 * avoid short-circuiting here
251 */
252 for (const operand of eachInstructionValueOperand(value)) {
253 const reactive = reactiveIdentifiers.isReactive(operand);
254 hasReactiveInput ||= reactive;
255 }
256
257 /**
258 * Hooks and the 'use' operator are sources of reactivity because
259 * they can access state (for hooks) or context (for hooks/use).
260 *
261 * Technically, `use` could be used to await a non-reactive promise,
262 * but we are conservative and assume that the value could be reactive.
263 */
264 if (
265 value.kind === 'CallExpression' &&
266 (getHookKind(fn.env, value.callee.identifier) != null ||
267 isUseOperator(value.callee.identifier))
268 ) {
269 hasReactiveInput = true;
270 } else if (
271 value.kind === 'MethodCall' &&
272 (getHookKind(fn.env, value.property.identifier) != null ||
273 isUseOperator(value.property.identifier))
274 ) {
275 hasReactiveInput = true;
276 }
277
278 if (hasReactiveInput) {
279 for (const lvalue of eachInstructionLValue(instruction)) {
280 /**
281 * Note that it's not correct to mark all stable-typed identifiers
282 * as non-reactive, since ternaries and other value blocks can
283 * produce reactive identifiers typed as these.
284 * (e.g. `props.cond ? setState1 : setState2`)
285 */
286 if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
287 continue;
288 }
289 reactiveIdentifiers.markReactive(lvalue);
290 }
291 }
292 if (hasReactiveInput || hasReactiveControl) {
293 for (const operand of eachInstructionValueOperand(value)) {
294 switch (operand.effect) {
295 case Effect.Capture:
296 case Effect.Store:
297 case Effect.ConditionallyMutate:
298 case Effect.ConditionallyMutateIterator:
299 case Effect.Mutate: {
300 if (isMutable(instruction, operand)) {
301 reactiveIdentifiers.markReactive(operand);
302 }
303 break;
304 }
305 case Effect.Freeze:
306 case Effect.Read: {
307 // no-op
308 break;
309 }
310 case Effect.Unknown: {
311 CompilerError.invariant(false, {
312 reason: 'Unexpected unknown effect',
313 loc: operand.loc,
314 });
315 }
316 default: {
317 assertExhaustive(
318 operand.effect,
319 `Unexpected effect kind \`${operand.effect}\``,
320 );
321 }
322 }
323 }
324 }
325 }
326 for (const operand of eachTerminalOperand(block.terminal)) {
327 reactiveIdentifiers.isReactive(operand);
328 }
329 }
330 } while (reactiveIdentifiers.snapshot());
331
332 function propagateReactivityToInnerFunctions(
333 fn: HIRFunction,
334 isOutermost: boolean,
335 ): void {
336 for (const [, block] of fn.body.blocks) {
337 for (const instr of block.instructions) {
338 if (!isOutermost) {
339 for (const operand of eachInstructionOperand(instr)) {
340 reactiveIdentifiers.isReactive(operand);
341 }
342 }
343 if (
344 instr.value.kind === 'ObjectMethod' ||
345 instr.value.kind === 'FunctionExpression'
346 ) {
347 propagateReactivityToInnerFunctions(
348 instr.value.loweredFunc.func,
349 false,
350 );
351 }
352 }
353 if (!isOutermost) {
354 for (const operand of eachTerminalOperand(block.terminal)) {
355 reactiveIdentifiers.isReactive(operand);
356 }
357 }
358 }
359 }
360
361 /**
362 * Propagate reactivity for inner functions, as we eventually hoist and dedupe
363 * dependency instructions for scopes.
364 */
365 propagateReactivityToInnerFunctions(fn, true);
366 }
367
368 class ReactivityMap {
369 hasChanges: boolean = false;
370 reactive: Set<IdentifierId> = new Set();
371
372 /**
373 * Sets of mutably aliased identifiers — these are the same foundation for determining
374 * reactive scopes a few passes later. The actual InferReactiveScopeVariables pass runs
375 * after LeaveSSA, which artificially merges mutable ranges in cases such as declarations
376 * that are later reassigned. Here we use only the underlying sets of mutably aliased values.
377 *
378 * Any identifier that has a mapping in this disjoint set will be treated as a stand in for
379 * its canonical identifier in all cases, so that any reactivity flowing into one identifier of
380 * an alias group will effectively make the whole alias group (all its identifiers) reactive.
381 */
382 aliasedIdentifiers: DisjointSet<Identifier>;
383
384 constructor(aliasedIdentifiers: DisjointSet<Identifier>) {
385 this.aliasedIdentifiers = aliasedIdentifiers;
386 }
387
388 isReactive(place: Place): boolean {
389 const identifier =
390 this.aliasedIdentifiers.find(place.identifier) ?? place.identifier;
391 const reactive = this.reactive.has(identifier.id);
392 if (reactive) {
393 place.reactive = true;
394 }
395 return reactive;
396 }
397
398 markReactive(place: Place): void {
399 place.reactive = true;
400 const identifier =
401 this.aliasedIdentifiers.find(place.identifier) ?? place.identifier;
402 if (!this.reactive.has(identifier.id)) {
403 this.hasChanges = true;
404 this.reactive.add(identifier.id);
405 }
406 }
407
408 snapshot(): boolean {
409 const hasChanges = this.hasChanges;
410 this.hasChanges = false;
411 return hasChanges;
412 }
413 }