main
md 559 lines 18.3 KB
Rendered Raw
1 # The Mutability & Aliasing Model
2
3 This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes).
4
5 To build an intuition, consider the following example:
6
7 ```js
8 function Component() {
9 // a is created and mutated over the course of these two instructions:
10 const a = {};
11 mutate(a);
12
13 // b and c are created and mutated together — mutate might modify b via c
14 const b = {};
15 const c = {b};
16 mutate(c);
17
18 // does not modify a/b/c
19 return <Foo a={a} c={c} />
20 }
21 ```
22
23 The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c.
24
25 In code, the mutability and aliasing model is compromised of the following phases:
26
27 * `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen.
28 * `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values.
29 * `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values.
30
31 Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions.
32
33 ## Mutation and Aliasing Effects
34
35 The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time
36
37 ### Creation Effects
38
39 #### Create
40
41 ```js
42 {
43 kind: 'Create';
44 into: Place;
45 value: ValueKind;
46 reason: ValueReason;
47 }
48 ```
49
50 Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`.
51
52 #### CreateFunction
53
54 ```js
55 {
56 kind: 'CreateFunction';
57 captures: Array<Place>;
58 function: FunctionExpression | ObjectMethod;
59 into: Place;
60 }
61 ```
62
63 Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`.
64
65 #### Apply
66
67 ```js
68 {
69 kind: 'Apply';
70 receiver: Place;
71 function: Place; // same as receiver for function calls
72 mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default
73 args: Array<Place | SpreadPattern | Hole>;
74 into: Place; // where result is stored
75 signature: FunctionSignature | null;
76 }
77 ```
78
79 Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine:
80
81 * If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments.
82 * Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects.
83 * Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those.
84 * Else fall back to inferring a generic set of effects.
85
86 The generic fallback is to assume:
87 - The return value may alias any of the arguments (Alias param -> return)
88 - Any arguments *may* be transitively mutated (MutateTransitiveConditionally param)
89 - Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M)
90
91 ### Aliasing Effects
92
93 These effects describe data-flow only, separately from mutation or other state-changing semantics.
94
95 #### Assign
96
97 ```js
98 {
99 kind: 'Assign';
100 from: Place;
101 into: Place;
102 }
103 ```
104
105 Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value.
106
107 > TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect
108
109 #### Alias
110
111 ```js
112 {
113 kind: 'Alias';
114 from: Place;
115 into: Place;
116 }
117 ```
118
119 Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value.
120
121 #### Capture
122
123 ```js
124 {
125 kind: 'Capture';
126 from: Place;
127 into: Place;
128 }
129 ```
130
131 Describes that a reference to one variable (from) is stored within another value (into). Examples include:
132 - An array expression captures the items of the array (`array = [capturedValue]`)
133 - Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`)
134 - Property assignment captures the value onto the object (`object.property = capturedValue`)
135
136 #### CreateFrom
137
138 ```js
139 {
140 kind: 'CreateFrom';
141 from: Place;
142 into: Place;
143 }
144 ```
145
146 This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include:
147
148 - Indexing into an array (`createdFrom = array[0]`)
149 - Reading an object property (`createdFrom = object.property`)
150 - Getting a Map key (`createdFrom = map.get(key)`)
151
152 #### ImmutableCapture
153
154 Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
155
156 ### MaybeAlias
157
158 Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
159
160 ### State-Changing Effects
161
162 The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
163
164 #### Freeze
165
166 ```js
167 {
168 kind: 'Freeze',
169 // The reference being frozen
170 value: Place;
171 // The reason the value is frozen (passed to a hook, passed to jsx, etc)
172 reason: ValueReason;
173 }
174 ```
175
176 Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update:
177
178 ```js
179 // INVALID DO NOT DO THIS
180 function Component(props) {
181 const array = useArray(props.value);
182 // OOPS! this value is memoized, the array won't get re-created
183 // when `props.value` changes, so we might just keep pushing new
184 // values to the same array on every render!
185 array.push(props.otherValue);
186 }
187
188 function useArray(a) {
189 return useMemo(() => [a], [a]);
190 }
191 ```
192
193 The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example:
194
195 ```js
196 const x = {};
197 const y = [];
198 x.y = y;
199 freeze(y); // y _reference_ is frozen
200 x.y.push(props.value); // but y is still considered mutable bc of this
201 ```
202
203 #### Mutate (and MutateConditionally)
204
205 ```js
206 {
207 kind: 'Mutate';
208 value: Place;
209 }
210 ```
211
212 Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include:
213
214 - Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!)
215 - Assigning a value to an object property modifies the object, but not any values stored in the object's other properties.
216
217 This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures.
218
219 `MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness.
220
221
222
223 #### MutateTransitiveConditionally (and MutateTransitive)
224
225 `MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them.
226
227 There is also `MutateTransitive` for completeness, but this is not generally used.
228
229 ### Side Effects
230
231 Finally, there are a few effects that describe error, or potential error, conditions:
232
233 - `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated.
234 - `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
235 - `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect.
236 - TODO: we could probably merge this and MutateGlobal
237 - `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs.
238
239
240 ## Rules
241
242 ### Mutation of Alias Mutates the Source Value
243
244 ```
245 Alias a <- b
246 Mutate a
247 =>
248 Mutate b
249 ```
250
251 Example:
252
253 ```js
254 const a = maybeIdentity(b); // Alias a <- b
255 a.property = value; // a could be b, so this mutates b
256 ```
257
258 ### Mutation of Assignment Mutates the Source Value
259
260 ```
261 Assign a <- b
262 Mutate a
263 =>
264 Mutate b
265 ```
266
267 Example:
268
269 ```js
270 const a = b;
271 a.property = value // a _is_ b, this mutates b
272 ```
273
274 ### Mutation of CreateFrom Mutates the Source Value
275
276 ```
277 CreateFrom a <- b
278 Mutate a
279 =>
280 Mutate b
281 ```
282
283 Example:
284
285 ```js
286 const a = b[index];
287 a.property = value // the contents of b are transitively mutated
288 ```
289
290
291 ### Mutation of Capture Does *Not* Mutate the Source Value
292
293 ```
294 Capture a <- b
295 Mutate a
296 !=>
297 ~Mutate b~
298 ```
299
300 Example:
301
302 ```js
303 const a = {};
304 a.b = b;
305 a.property = value; // mutates a, not b
306 ```
307
308 ### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture
309
310 ```
311 Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
312 Mutate b
313 =>
314 Mutate a
315 ```
316
317 A derived value changes when it's source value is mutated.
318
319 Example:
320
321 ```js
322 const x = {};
323 const y = [x];
324 x.y = true; // this changes the value within `y` ie mutates y
325 ```
326
327
328 ### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source
329
330 ```
331 Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b
332 MutateTransitive a
333 =>
334 MutateTransitive b
335 ```
336
337 Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then:
338
339 ```js
340 const a = b; // assign
341 mutate(a); // clearly can transitively mutate b
342
343 const a = maybeIdentity(b); // alias
344 mutate(a); // clearly can transitively mutate b
345
346 const a = b[index]; // createfrom
347 mutate(a); // clearly can transitively mutate b
348
349 const a = {};
350 a.b = b; // capture
351 mutate(a); // can transitively mutate b
352 ```
353
354 ### MaybeAlias makes mutation conditional
355
356 Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
357
358 ```
359 MaybeAlias a <- b
360 Mutate a
361 =>
362 MutateConditional b
363 ```
364
365 ### Freeze Does Not Freeze the Value
366
367 Freeze does not freeze the value itself:
368
369 ```
370 Create x
371 Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
372 Freeze y
373 !=>
374 ~Freeze x~
375 ```
376
377 This means that subsequent mutations of the original value are valid:
378
379 ```
380 Create x
381 Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
382 Freeze y
383 Mutate x
384 =>
385 Mutate x (mutation is ok)
386 ```
387
388 As well as mutations through other assignments/aliases/captures/createfroms of the original value:
389
390 ```
391 Create x
392 Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
393 Freeze y
394 Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x
395 Mutate z
396 =>
397 Mutate x (mutation is ok)
398 ```
399
400 ### Freeze Freezes The Reference
401
402 Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate.
403
404 Conditional mutations of the reference are no-ops:
405
406 ```
407 Create x
408 Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
409 Freeze y
410 MutateConditional y
411 =>
412 (no mutation)
413 ```
414
415 And known mutations of the reference are errors:
416
417 ```
418 Create x
419 Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x
420 Freeze y
421 MutateConditional y
422 =>
423 MutateFrozen y error=...
424 ```
425
426 ### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture
427
428 A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables.
429
430 In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example:
431
432 ```js
433 const f = (x) => {
434 const y = [x]; // capture y <- x
435 const z = y[0]; // createfrom z <- y
436 return z; // assign return <- z
437 }
438 // <Effect> return <- x
439 ```
440
441 In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source.
442
443 #### Each of the effects is transitive with itself
444
445 ```
446 Assign b <- a
447 Assign c <- b
448 =>
449 Assign c <- a
450 ```
451
452 ```
453 Alias b <- a
454 Alias c <- b
455 =>
456 Alias c <- a
457 ```
458
459 ```
460 CreateFrom b <- a
461 CreateFrom c <- b
462 =>
463 CreateFrom c <- a
464 ```
465
466 ```
467 Capture b <- a
468 Capture c <- b
469 =>
470 Capture c <- a
471 ```
472
473 #### Alias > Assign
474
475 ```
476 Assign b <- a
477 Alias c <- b
478 =>
479 Alias c <- a
480 ```
481
482 ```
483 Alias b <- a
484 Assign c <- b
485 =>
486 Alias c <- a
487 ```
488
489 ### CreateFrom > Assign/Alias
490
491 Intuition:
492
493 ```
494 CreateFrom b <- a
495 Alias c <- b OR Assign c <- b
496 =>
497 CreateFrom c <- a
498 ```
499
500 ```
501 Alias b <- a OR Assign b <- a
502 CreateFrom c <- b
503 =>
504 CreateFrom c <- a
505 ```
506
507 ### Capture > Assign/Alias
508
509 Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture.
510
511 ```
512 Capture b <- a
513 Alias c <- b OR Assign c <- b
514 =>
515 Capture c <- a
516 ```
517
518 ```
519 Alias b <- a OR Assign b <- a
520 Capture c <- b
521 =>
522 Capture c <- a
523 ```
524
525 ### Capture And CreateFrom
526
527 Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations:
528
529 Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original.
530
531 ```js
532 const b = [a]; // capture
533 const c = b[0]; // createfrom
534 mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
535 ```
536
537 We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias.
538
539 ```
540 Capture b <- a
541 CreateFrom c <- b
542 =>
543 Alias c <- a
544 ```
545
546 Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
547
548 ```js
549 const b = a[0]; // createfrom
550 const c = [b]; // capture
551 mutate(c); // does not mutate a, so the result must be Capture
552 ```
553
554 ```
555 CreateFrom b <- a
556 Capture c <- b
557 =>
558 Capture c <- a
559 ```