@samitouri / QOS-React-1 / commits / 94cf60bede

[compiler] New inference repros/fixes (#33584)

Substantially improves the last major known issue with the new inference model's implementation: inferring effects of function expressions. I knowingly used a really simple (dumb) approach in InferFunctionExpressionAliasingEffects but it worked surprisingly well on a ton of code. However, investigating during the sync I saw that we the algorithm was literally running out of memory, or crashing from arrays that exceeded the maximum capacity. We were accumluating data flow in a way that could lead to lists of data flow captures compounding on themselves and growing very large very quickly. Plus, we were incorrectly recording some data flow, leading to cases where we reported false positive "can't mutate frozen value" for example. So I went back to the drawing board. InferMutationAliasingRanges already builds up a data flow graph which it uses to figure out what values would be affected by mutations of other values, and update mutable ranges. Well, the key question that we really want to answer for inferring a function expression's aliasing effects is which values alias/capture where. Per the docs I wrote up, we only have to record such aliasing _if they are observable via mutations_. So, lightbulb: simulate mutations of the params, free variables, and return of the function expression and see which params/free-vars would be affected! That's what we do now, giving us precise information about which such values alias/capture where. When the "into" is a param/context-var we use Capture, iwhen the destination is the return we use Alias to be conservative. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33584). * #33626 * #33625 * #33624 * __->__ #33584

Joseph Savona committed Jun 24, 2025 at 10:01 UTC 94cf60bede7cd6685e07a4374d1e3aa90445130b
18 files changed +746 -265
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+4
@@ -1770,6 +1770,10 @@ export function isUseStateType(id: Identifier): boolean {
1770 return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
1771 }
1772
1773 +export function isJsxType(type: Type): boolean {
1774 + return type.kind === 'Object' && type.shapeId === 'BuiltInJsx';
1775 +}
1776 +
1777 export function isRefOrRefValue(id: Identifier): boolean {
1778 return isUseRefType(id) || isRefValueType(id);
1779 }
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+12 -26
@@ -20,11 +20,9 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
20 import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
21 import {inferMutableRanges} from './InferMutableRanges';
22 import inferReferenceEffects from './InferReferenceEffects';
23 -import {assertExhaustive, retainWhere} from '../Utils/utils';
23 +import {assertExhaustive} from '../Utils/utils';
24 import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
25 -import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature';
25 import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
27 -import {hashEffect} from './AliasingEffects';
26
27 export default function analyseFunctions(func: HIRFunction): void {
28 for (const [_, block] of func.body.blocks) {
@@ -69,30 +67,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
67 analyseFunctions(fn);
68 inferMutationAliasingEffects(fn, {isFunctionExpression: true});
69 deadCodeElimination(fn);
72 - inferMutationAliasingRanges(fn, {isFunctionExpression: true});
70 + const functionEffects = inferMutationAliasingRanges(fn, {
71 + isFunctionExpression: true,
72 + }).unwrap();
73 rewriteInstructionKindsBasedOnReassignment(fn);
74 inferReactiveScopeVariables(fn);
75 - const effects = inferFunctionExpressionAliasingEffectsSignature(fn);
76 - fn.env.logger?.debugLogIRs?.({
77 - kind: 'hir',
78 - name: 'AnalyseFunction (inner)',
79 - value: fn,
80 - });
81 - if (effects != null) {
82 - fn.aliasingEffects ??= [];
83 - fn.aliasingEffects?.push(...effects);
84 - }
85 - if (fn.aliasingEffects != null) {
86 - const seen = new Set<string>();
87 - retainWhere(fn.aliasingEffects, effect => {
88 - const hash = hashEffect(effect);
89 - if (seen.has(hash)) {
90 - return false;
91 - }
92 - seen.add(hash);
93 - return true;
94 - });
95 - }
75 + fn.aliasingEffects = functionEffects;
76
77 /**
78 * Phase 2: populate the Effect of each context variable to use in inferring
@@ -100,7 +80,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
80 * effects to decide if the function may be mutable or not.
81 */
82 const capturedOrMutated = new Set<IdentifierId>();
103 - for (const effect of effects ?? []) {
83 + for (const effect of functionEffects) {
84 switch (effect.kind) {
85 case 'Assign':
86 case 'Alias':
@@ -152,6 +132,12 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
132 operand.effect = Effect.Read;
133 }
134 }
135 +
136 + fn.env.logger?.debugLogIRs?.({
137 + kind: 'hir',
138 + name: 'AnalyseFunction (inner)',
139 + value: fn,
140 + });
141 }
142
143 function lower(func: HIRFunction): void {
compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionExpressionAliasingEffectsSignature.ts deleted
-206
@@ -1,206 +0,0 @@
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 {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
9 -import {getOrInsertDefault} from '../Utils/utils';
10 -import {AliasingEffect} from './AliasingEffects';
11 -
12 -/**
13 - * This function tracks data flow within an inner function expression in order to
14 - * compute a set of data-flow aliasing effects describing data flow between the function's
15 - * params, context variables, and return value.
16 - *
17 - * For example, consider the following function expression:
18 - *
19 - * ```
20 - * (x) => { return [x, y] }
21 - * ```
22 - *
23 - * This function captures both param `x` and context variable `y` into the return value.
24 - * Unlike our previous inference which counted this as a mutation of x and y, we want to
25 - * build a signature for the function that describes the data flow. We would infer
26 - * `Capture x -> return, Capture y -> return` effects for this function.
27 - *
28 - * This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
29 - * from instructions within the function up to the function itself.
30 - */
31 -export function inferFunctionExpressionAliasingEffectsSignature(
32 - fn: HIRFunction,
33 -): Array<AliasingEffect> | null {
34 - const effects: Array<AliasingEffect> = [];
35 -
36 - /**
37 - * Map used to identify tracked variables: params, context vars, return value
38 - * This is used to detect mutation/capturing/aliasing of params/context vars
39 - */
40 - const tracked = new Map<IdentifierId, Place>();
41 - tracked.set(fn.returns.identifier.id, fn.returns);
42 - for (const operand of [...fn.context, ...fn.params]) {
43 - const place = operand.kind === 'Identifier' ? operand : operand.place;
44 - tracked.set(place.identifier.id, place);
45 - }
46 -
47 - /**
48 - * Track capturing/aliasing of context vars and params into each other and into the return.
49 - * We don't need to track locals and intermediate values, since we're only concerned with effects
50 - * as they relate to arguments visible outside the function.
51 - *
52 - * For each aliased identifier we track capture/alias/createfrom and then merge this with how
53 - * the value is used. Eg capturing an alias => capture. See joinEffects() helper.
54 - */
55 - type AliasedIdentifier = {
56 - kind: AliasingKind;
57 - place: Place;
58 - };
59 - const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
60 -
61 - /*
62 - * Check for aliasing of tracked values. Also joins the effects of how the value is
63 - * used (@param kind) with the aliasing type of each value
64 - */
65 - function lookup(
66 - place: Place,
67 - kind: AliasedIdentifier['kind'],
68 - ): Array<AliasedIdentifier> | null {
69 - if (tracked.has(place.identifier.id)) {
70 - return [{kind, place}];
71 - }
72 - return (
73 - dataFlow.get(place.identifier.id)?.map(aliased => ({
74 - kind: joinEffects(aliased.kind, kind),
75 - place: aliased.place,
76 - })) ?? null
77 - );
78 - }
79 -
80 - // todo: fixpoint
81 - for (const block of fn.body.blocks.values()) {
82 - for (const phi of block.phis) {
83 - const operands: Array<AliasedIdentifier> = [];
84 - for (const operand of phi.operands.values()) {
85 - const inputs = lookup(operand, 'Alias');
86 - if (inputs != null) {
87 - operands.push(...inputs);
88 - }
89 - }
90 - if (operands.length !== 0) {
91 - dataFlow.set(phi.place.identifier.id, operands);
92 - }
93 - }
94 - for (const instr of block.instructions) {
95 - if (instr.effects == null) continue;
96 - for (const effect of instr.effects) {
97 - if (
98 - effect.kind === 'Assign' ||
99 - effect.kind === 'Capture' ||
100 - effect.kind === 'Alias' ||
101 - effect.kind === 'CreateFrom'
102 - ) {
103 - const from = lookup(effect.from, effect.kind);
104 - if (from == null) {
105 - continue;
106 - }
107 - const into = lookup(effect.into, 'Alias');
108 - if (into == null) {
109 - getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
110 - ...from,
111 - );
112 - } else {
113 - for (const aliased of into) {
114 - getOrInsertDefault(
115 - dataFlow,
116 - aliased.place.identifier.id,
117 - [],
118 - ).push(...from);
119 - }
120 - }
121 - } else if (
122 - effect.kind === 'Create' ||
123 - effect.kind === 'CreateFunction'
124 - ) {
125 - getOrInsertDefault(dataFlow, effect.into.identifier.id, [
126 - {kind: 'Alias', place: effect.into},
127 - ]);
128 - } else if (
129 - effect.kind === 'MutateFrozen' ||
130 - effect.kind === 'MutateGlobal' ||
131 - effect.kind === 'Impure' ||
132 - effect.kind === 'Render'
133 - ) {
134 - effects.push(effect);
135 - }
136 - }
137 - }
138 - if (block.terminal.kind === 'return') {
139 - const from = lookup(block.terminal.value, 'Alias');
140 - if (from != null) {
141 - getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
142 - ...from,
143 - );
144 - }
145 - }
146 - }
147 -
148 - // Create aliasing effects based on observed data flow
149 - let hasReturn = false;
150 - for (const [into, from] of dataFlow) {
151 - const input = tracked.get(into);
152 - if (input == null) {
153 - continue;
154 - }
155 - for (const aliased of from) {
156 - if (
157 - aliased.place.identifier.id === input.identifier.id ||
158 - !tracked.has(aliased.place.identifier.id)
159 - ) {
160 - continue;
161 - }
162 - const effect = {kind: aliased.kind, from: aliased.place, into: input};
163 - effects.push(effect);
164 - if (
165 - into === fn.returns.identifier.id &&
166 - (aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
167 - ) {
168 - hasReturn = true;
169 - }
170 - }
171 - }
172 - // TODO: more precise return effect inference
173 - if (!hasReturn) {
174 - effects.unshift({
175 - kind: 'Create',
176 - into: fn.returns,
177 - value:
178 - fn.returnType.kind === 'Primitive'
179 - ? ValueKind.Primitive
180 - : ValueKind.Mutable,
181 - reason: ValueReason.KnownReturnSignature,
182 - });
183 - }
184 -
185 - return effects;
186 -}
187 -
188 -export enum MutationKind {
189 - None = 0,
190 - Conditional = 1,
191 - Definite = 2,
192 -}
193 -
194 -type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
195 -function joinEffects(
196 - effect1: AliasingKind,
197 - effect2: AliasingKind,
198 -): AliasingKind {
199 - if (effect1 === 'Capture' || effect2 === 'Capture') {
200 - return 'Capture';
201 - } else if (effect1 === 'Assign' || effect2 === 'Assign') {
202 - return 'Assign';
203 - } else {
204 - return 'Alias';
205 - }
206 -}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+2 -3
@@ -822,7 +822,8 @@ function applyEffect(
822 const functionValues = state.values(effect.function);
823 if (
824 functionValues.length === 1 &&
825 - functionValues[0].kind === 'FunctionExpression'
825 + functionValues[0].kind === 'FunctionExpression' &&
826 + functionValues[0].loweredFunc.func.aliasingEffects != null
827 ) {
828 /*
829 * We're calling a locally declared function, we already know it's effects!
@@ -2126,8 +2127,6 @@ function computeEffectsForLegacySignature(
2127 const mutateIterator = conditionallyMutateIterator(place);
2128 if (mutateIterator != null) {
2129 effects.push(mutateIterator);
2129 - // TODO: should we always push to captures?
2130 - captures.push(place);
2130 }
2131 effects.push({
2132 kind: 'Capture',
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts
+135 -20
@@ -13,7 +13,10 @@ import {
13 Identifier,
14 IdentifierId,
15 InstructionId,
16 + isJsxType,
17 makeInstructionId,
18 + ValueKind,
19 + ValueReason,
20 Place,
21 } from '../HIR/HIR';
22 import {
@@ -22,34 +25,58 @@ import {
25 eachTerminalOperand,
26 } from '../HIR/visitors';
27 import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
25 -import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature';
26 -import {Result} from '../Utils/Result';
28 +import {Err, Ok, Result} from '../Utils/Result';
29 +import {AliasingEffect} from './AliasingEffects';
30
31 /**
29 - * Infers mutable ranges for all values in the program, using previously inferred
30 - * mutation/aliasing effects. This pass builds a data flow graph using the effects,
31 - * tracking an abstract notion of "when" each effect occurs relative to the others.
32 - * It then walks each mutation effect against the graph, updating the range of each
33 - * node that would be reachable at the "time" that the effect occurred.
32 + * This pass builds an abstract model of the heap and interprets the effects of the
33 + * given function in order to determine the following:
34 + * - The mutable ranges of all identifiers in the function
35 + * - The externally-visible effects of the function, such as mutations of params and
36 + * context-vars, aliasing between params/context-vars/return-value, and impure side
37 + * effects.
38 + * - The legacy `Effect` to store on each Place.
39 + *
40 + * This pass builds a data flow graph using the effects, tracking an abstract notion
41 + * of "when" each effect occurs relative to the others. It then walks each mutation
42 + * effect against the graph, updating the range of each node that would be reachable
43 + * at the "time" that the effect occurred.
44 *
45 * This pass also validates against invalid effects: any function that is reachable
46 * by being called, or via a Render effect, is validated against mutating globals
47 * or calling impure code.
48 *
49 * Note that this function also populates the outer function's aliasing effects with
40 - * any mutations that apply to its params or context variables. For example, a
41 - * function expression such as the following:
50 + * any mutations that apply to its params or context variables.
51 + *
52 + * ## Example
53 + * A function expression such as the following:
54 *
55 * ```
56 * (x) => { x.y = true }
57 * ```
58 *
59 * Would populate a `Mutate x` aliasing effect on the outer function.
60 + *
61 + * ## Returned Function Effects
62 + *
63 + * The function returns (if successful) a list of externally-visible effects.
64 + * This is determined by simulating a conditional, transitive mutation against
65 + * each param, context variable, and return value in turn, and seeing which other
66 + * such values are affected. If they're affected, they must be captured, so we
67 + * record a Capture.
68 + *
69 + * The only tricky bit is the return value, which could _alias_ (or even assign)
70 + * one or more of the params/context-vars rather than just capturing. So we have
71 + * to do a bit more tracking for returns.
72 */
73 export function inferMutationAliasingRanges(
74 fn: HIRFunction,
75 {isFunctionExpression}: {isFunctionExpression: boolean},
52 -): Result<void, CompilerError> {
76 +): Result<Array<AliasingEffect>, CompilerError> {
77 + // The set of externally-visible effects
78 + const functionEffects: Array<AliasingEffect> = [];
79 +
80 /**
81 * Part 1: Infer mutable ranges for values. We build an abstract model of
82 * values, the alias/capture edges between them, and the set of mutations.
@@ -168,8 +195,10 @@ export function inferMutationAliasingRanges(
195 effect.kind === 'Impure'
196 ) {
197 errors.push(effect.error);
198 + functionEffects.push(effect);
199 } else if (effect.kind === 'Render') {
200 renders.push({index: index++, place: effect.place});
201 + functionEffects.push(effect);
202 }
203 }
204 }
@@ -215,7 +244,6 @@ export function inferMutationAliasingRanges(
244 for (const render of renders) {
245 state.render(render.index, render.place.identifier, errors);
246 }
218 - fn.aliasingEffects ??= [];
247 for (const param of [...fn.context, ...fn.params]) {
248 const place = param.kind === 'Identifier' ? param : param.place;
249 const node = state.nodes.get(place.identifier);
@@ -226,13 +254,13 @@ export function inferMutationAliasingRanges(
254 if (node.local != null) {
255 if (node.local.kind === MutationKind.Conditional) {
256 mutated = true;
229 - fn.aliasingEffects.push({
257 + functionEffects.push({
258 kind: 'MutateConditionally',
259 value: {...place, loc: node.local.loc},
260 });
261 } else if (node.local.kind === MutationKind.Definite) {
262 mutated = true;
235 - fn.aliasingEffects.push({
263 + functionEffects.push({
264 kind: 'Mutate',
265 value: {...place, loc: node.local.loc},
266 });
@@ -241,13 +269,13 @@ export function inferMutationAliasingRanges(
269 if (node.transitive != null) {
270 if (node.transitive.kind === MutationKind.Conditional) {
271 mutated = true;
244 - fn.aliasingEffects.push({
272 + functionEffects.push({
273 kind: 'MutateTransitiveConditionally',
274 value: {...place, loc: node.transitive.loc},
275 });
276 } else if (node.transitive.kind === MutationKind.Definite) {
277 mutated = true;
250 - fn.aliasingEffects.push({
278 + functionEffects.push({
279 kind: 'MutateTransitive',
280 value: {...place, loc: node.transitive.loc},
281 });
@@ -436,7 +464,82 @@ export function inferMutationAliasingRanges(
464 }
465 }
466
439 - return errors.asResult();
467 + /**
468 + * Part 3
469 + * Finish populating the externally visible effects. Above we bubble-up the side effects
470 + * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables.
471 + * Here we populate an effect to create the return value as well as populating alias/capture
472 + * effects for how data flows between the params, context vars, and return.
473 + */
474 + functionEffects.push({
475 + kind: 'Create',
476 + into: fn.returns,
477 + value:
478 + fn.returnType.kind === 'Primitive'
479 + ? ValueKind.Primitive
480 + : isJsxType(fn.returnType)
481 + ? ValueKind.Frozen
482 + : ValueKind.Mutable,
483 + reason: ValueReason.KnownReturnSignature,
484 + });
485 + /**
486 + * Determine precise data-flow effects by simulating transitive mutations of the params/
487 + * captures and seeing what other params/context variables are affected. Anything that
488 + * would be transitively mutated needs a capture relationship.
489 + */
490 + const tracked: Array<Place> = [];
491 + const ignoredErrors = new CompilerError();
492 + for (const param of [...fn.params, ...fn.context, fn.returns]) {
493 + const place = param.kind === 'Identifier' ? param : param.place;
494 + tracked.push(place);
495 + }
496 + for (const into of tracked) {
497 + const mutationIndex = index++;
498 + state.mutate(
499 + mutationIndex,
500 + into.identifier,
501 + null,
502 + true,
503 + MutationKind.Conditional,
504 + into.loc,
505 + ignoredErrors,
506 + );
507 + for (const from of tracked) {
508 + if (
509 + from.identifier.id === into.identifier.id ||
510 + from.identifier.id === fn.returns.identifier.id
511 + ) {
512 + continue;
513 + }
514 + const fromNode = state.nodes.get(from.identifier);
515 + CompilerError.invariant(fromNode != null, {
516 + reason: `Expected a node to exist for all parameters and context variables`,
517 + loc: into.loc,
518 + });
519 + if (fromNode.lastMutated === mutationIndex) {
520 + if (into.identifier.id === fn.returns.identifier.id) {
521 + // The return value could be any of the params/context variables
522 + functionEffects.push({
523 + kind: 'Alias',
524 + from,
525 + into,
526 + });
527 + } else {
528 + // Otherwise params/context-vars can only capture each other
529 + functionEffects.push({
530 + kind: 'Capture',
531 + from,
532 + into,
533 + });
534 + }
535 + }
536 + }
537 + }
538 +
539 + if (errors.hasErrors() && !isFunctionExpression) {
540 + return Err(errors);
541 + }
542 + return Ok(functionEffects);
543 }
544
545 function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
@@ -452,6 +555,12 @@ function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
555 }
556 }
557
558 +export enum MutationKind {
559 + None = 0,
560 + Conditional = 1,
561 + Definite = 2,
562 +}
563 +
564 type Node = {
565 id: Identifier;
566 createdFrom: Map<Identifier, number>;
@@ -460,6 +569,7 @@ type Node = {
569 edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
570 transitive: {kind: MutationKind; loc: SourceLocation} | null;
571 local: {kind: MutationKind; loc: SourceLocation} | null;
572 + lastMutated: number;
573 value:
574 | {kind: 'Object'}
575 | {kind: 'Phi'}
@@ -477,6 +587,7 @@ class AliasingState {
587 edges: [],
588 transitive: null,
589 local: null,
590 + lastMutated: 0,
591 value,
592 });
593 }
@@ -558,7 +669,8 @@ class AliasingState {
669 mutate(
670 index: number,
671 start: Identifier,
561 - end: InstructionId,
672 + // Null is used for simulated mutations
673 + end: InstructionId | null,
674 transitive: boolean,
675 kind: MutationKind,
676 loc: SourceLocation,
@@ -580,9 +692,12 @@ class AliasingState {
692 if (node == null) {
693 continue;
694 }
583 - node.id.mutableRange.end = makeInstructionId(
584 - Math.max(node.id.mutableRange.end, end),
585 - );
695 + node.lastMutated = Math.max(node.lastMutated, index);
696 + if (end != null) {
697 + node.id.mutableRange.end = makeInstructionId(
698 + Math.max(node.id.mutableRange.end, end),
699 + );
700 + }
701 if (
702 node.value.kind === 'Function' &&
703 node.transitive == null &&
compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md
+10 -10
@@ -514,9 +514,9 @@ Intuition: these effects are inverses of each other (capturing into an object, e
514 Capture then CreatFrom 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.
515
516 ```js
517 -const y = [x]; // capture
518 -const z = y[0]; // createfrom
519 -mutate(z); // this clearly can mutate x, so the result must be one of Assign/Alias/CreateFrom
517 +const b = [a]; // capture
518 +const c = b[0]; // createfrom
519 +mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom
520 ```
521
522 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.
@@ -528,17 +528,17 @@ CreateFrom c <- b
528 Alias c <- a
529 ```
530
531 -Meanwhile the opposite direction preservers the capture, because the result is not the same as the source:
531 +Meanwhile the opposite direction preserves the capture, because the result is not the same as the source:
532
533 ```js
534 -const y = x[0]; // createfrom
535 -const z = [y]; // capture
536 -mutate(z); // does not mutate x, so the result must be Capture
534 +const b = a[0]; // createfrom
535 +const c = [b]; // capture
536 +mutate(c); // does not mutate a, so the result must be Capture
537 ```
538
539 ```
540 -Capture b <- a
541 -CreateFrom c <- b
540 +CreateFrom b <- a
541 +Capture c <- b
542 =>
543 -Capture b <- a
543 +Capture c <- a
544 ```
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md new
+81
@@ -0,0 +1,81 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, mutate} from 'shared-runtime';
6 +
7 +function Component({foo, bar}) {
8 + let x = {foo};
9 + let y = {bar};
10 + const f0 = function () {
11 + let a = {y};
12 + let b = {x};
13 + a.y.x = b;
14 + };
15 + f0();
16 + mutate(y);
17 + return <Stringify x={y} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Component,
22 + params: [{foo: 2, bar: 3}],
23 + sequentialRenders: [
24 + {foo: 2, bar: 3},
25 + {foo: 2, bar: 3},
26 + {foo: 2, bar: 4},
27 + {foo: 3, bar: 4},
28 + ],
29 +};
30 +
31 +```
32 +
33 +## Code
34 +
35 +```javascript
36 +import { c as _c } from "react/compiler-runtime";
37 +import { Stringify, mutate } from "shared-runtime";
38 +
39 +function Component(t0) {
40 + const $ = _c(3);
41 + const { foo, bar } = t0;
42 + let t1;
43 + if ($[0] !== bar || $[1] !== foo) {
44 + const x = { foo };
45 + const y = { bar };
46 + const f0 = function () {
47 + const a = { y };
48 + const b = { x };
49 + a.y.x = b;
50 + };
51 +
52 + f0();
53 + mutate(y);
54 + t1 = <Stringify x={y} />;
55 + $[0] = bar;
56 + $[1] = foo;
57 + $[2] = t1;
58 + } else {
59 + t1 = $[2];
60 + }
61 + return t1;
62 +}
63 +
64 +export const FIXTURE_ENTRYPOINT = {
65 + fn: Component,
66 + params: [{ foo: 2, bar: 3 }],
67 + sequentialRenders: [
68 + { foo: 2, bar: 3 },
69 + { foo: 2, bar: 3 },
70 + { foo: 2, bar: 4 },
71 + { foo: 3, bar: 4 },
72 + ],
73 +};
74 +
75 +```
76 +
77 +### Eval output
78 +(kind: ok) <div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
79 +<div>{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
80 +<div>{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}</div>
81 +<div>{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js new
+25
@@ -0,0 +1,25 @@
1 +import {Stringify, mutate} from 'shared-runtime';
2 +
3 +function Component({foo, bar}) {
4 + let x = {foo};
5 + let y = {bar};
6 + const f0 = function () {
7 + let a = {y};
8 + let b = {x};
9 + a.y.x = b;
10 + };
11 + f0();
12 + mutate(y);
13 + return <Stringify x={y} />;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{foo: 2, bar: 3}],
19 + sequentialRenders: [
20 + {foo: 2, bar: 3},
21 + {foo: 2, bar: 3},
22 + {foo: 2, bar: 4},
23 + {foo: 3, bar: 4},
24 + ],
25 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md new
+97
@@ -0,0 +1,97 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useMemo} from 'react';
6 +import {identity, ValidateMemoization} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const x = useMemo(() => ({a}), [a, b]);
10 + const f = () => {
11 + return identity(x);
12 + };
13 + const x2 = f();
14 + x2.b = b;
15 +
16 + return <ValidateMemoization inputs={[a, b]} output={x} />;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{a: 0, b: 0}],
22 + sequentialRenders: [
23 + {a: 0, b: 0},
24 + {a: 0, b: 1},
25 + {a: 1, b: 1},
26 + {a: 0, b: 0},
27 + ],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +import { c as _c } from "react/compiler-runtime";
36 +import { useMemo } from "react";
37 +import { identity, ValidateMemoization } from "shared-runtime";
38 +
39 +function Component(t0) {
40 + const $ = _c(10);
41 + const { a, b } = t0;
42 + let t1;
43 + let x;
44 + if ($[0] !== a || $[1] !== b) {
45 + t1 = { a };
46 + x = t1;
47 + const f = () => identity(x);
48 +
49 + const x2 = f();
50 + x2.b = b;
51 + $[0] = a;
52 + $[1] = b;
53 + $[2] = x;
54 + $[3] = t1;
55 + } else {
56 + x = $[2];
57 + t1 = $[3];
58 + }
59 + let t2;
60 + if ($[4] !== a || $[5] !== b) {
61 + t2 = [a, b];
62 + $[4] = a;
63 + $[5] = b;
64 + $[6] = t2;
65 + } else {
66 + t2 = $[6];
67 + }
68 + let t3;
69 + if ($[7] !== t2 || $[8] !== x) {
70 + t3 = <ValidateMemoization inputs={t2} output={x} />;
71 + $[7] = t2;
72 + $[8] = x;
73 + $[9] = t3;
74 + } else {
75 + t3 = $[9];
76 + }
77 + return t3;
78 +}
79 +
80 +export const FIXTURE_ENTRYPOINT = {
81 + fn: Component,
82 + params: [{ a: 0, b: 0 }],
83 + sequentialRenders: [
84 + { a: 0, b: 0 },
85 + { a: 0, b: 1 },
86 + { a: 1, b: 1 },
87 + { a: 0, b: 0 },
88 + ],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
95 +<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
96 +<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
97 +<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js new
+24
@@ -0,0 +1,24 @@
1 +import {useMemo} from 'react';
2 +import {identity, ValidateMemoization} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const x = useMemo(() => ({a}), [a, b]);
6 + const f = () => {
7 + return identity(x);
8 + };
9 + const x2 = f();
10 + x2.b = b;
11 +
12 + return <ValidateMemoization inputs={[a, b]} output={x} />;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{a: 0, b: 0}],
18 + sequentialRenders: [
19 + {a: 0, b: 0},
20 + {a: 0, b: 1},
21 + {a: 1, b: 1},
22 + {a: 0, b: 0},
23 + ],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md new
+92
@@ -0,0 +1,92 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useMemo} from 'react';
6 +import {identity, ValidateMemoization} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const x = useMemo(() => ({a}), [a, b]);
10 + const x2 = identity(x);
11 + x2.b = b;
12 +
13 + return <ValidateMemoization inputs={[a, b]} output={x} />;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{a: 0, b: 0}],
19 + sequentialRenders: [
20 + {a: 0, b: 0},
21 + {a: 0, b: 1},
22 + {a: 1, b: 1},
23 + {a: 0, b: 0},
24 + ],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime";
33 +import { useMemo } from "react";
34 +import { identity, ValidateMemoization } from "shared-runtime";
35 +
36 +function Component(t0) {
37 + const $ = _c(10);
38 + const { a, b } = t0;
39 + let t1;
40 + let x;
41 + if ($[0] !== a || $[1] !== b) {
42 + t1 = { a };
43 + x = t1;
44 + const x2 = identity(x);
45 + x2.b = b;
46 + $[0] = a;
47 + $[1] = b;
48 + $[2] = x;
49 + $[3] = t1;
50 + } else {
51 + x = $[2];
52 + t1 = $[3];
53 + }
54 + let t2;
55 + if ($[4] !== a || $[5] !== b) {
56 + t2 = [a, b];
57 + $[4] = a;
58 + $[5] = b;
59 + $[6] = t2;
60 + } else {
61 + t2 = $[6];
62 + }
63 + let t3;
64 + if ($[7] !== t2 || $[8] !== x) {
65 + t3 = <ValidateMemoization inputs={t2} output={x} />;
66 + $[7] = t2;
67 + $[8] = x;
68 + $[9] = t3;
69 + } else {
70 + t3 = $[9];
71 + }
72 + return t3;
73 +}
74 +
75 +export const FIXTURE_ENTRYPOINT = {
76 + fn: Component,
77 + params: [{ a: 0, b: 0 }],
78 + sequentialRenders: [
79 + { a: 0, b: 0 },
80 + { a: 0, b: 1 },
81 + { a: 1, b: 1 },
82 + { a: 0, b: 0 },
83 + ],
84 +};
85 +
86 +```
87 +
88 +### Eval output
89 +(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
90 +<div>{"inputs":[0,1],"output":{"a":0,"b":1}}</div>
91 +<div>{"inputs":[1,1],"output":{"a":1,"b":1}}</div>
92 +<div>{"inputs":[0,0],"output":{"a":0,"b":0}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js new
+21
@@ -0,0 +1,21 @@
1 +import {useMemo} from 'react';
2 +import {identity, ValidateMemoization} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const x = useMemo(() => ({a}), [a, b]);
6 + const x2 = identity(x);
7 + x2.b = b;
8 +
9 + return <ValidateMemoization inputs={[a, b]} output={x} />;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{a: 0, b: 0}],
15 + sequentialRenders: [
16 + {a: 0, b: 0},
17 + {a: 0, b: 1},
18 + {a: 1, b: 1},
19 + {a: 0, b: 0},
20 + ],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md new
+60
@@ -0,0 +1,60 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + const x = {};
7 + const fn = () => {
8 + new Object()
9 + .build(x)
10 + .build({})
11 + .build({})
12 + .build({})
13 + .build({})
14 + .build({})
15 + .build({});
16 + };
17 + return <Stringify x={x} fn={fn} />;
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime";
26 +function Component() {
27 + const $ = _c(2);
28 + let t0;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + t0 = {};
31 + $[0] = t0;
32 + } else {
33 + t0 = $[0];
34 + }
35 + const x = t0;
36 + let t1;
37 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
38 + const fn = () => {
39 + new Object()
40 + .build(x)
41 + .build({})
42 + .build({})
43 + .build({})
44 + .build({})
45 + .build({})
46 + .build({});
47 + };
48 +
49 + t1 = <Stringify x={x} fn={fn} />;
50 + $[1] = t1;
51 + } else {
52 + t1 = $[1];
53 + }
54 + return t1;
55 +}
56 +
57 +```
58 +
59 +### Eval output
60 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js new
+14
@@ -0,0 +1,14 @@
1 +function Component() {
2 + const x = {};
3 + const fn = () => {
4 + new Object()
5 + .build(x)
6 + .build({})
7 + .build({})
8 + .build({})
9 + .build({})
10 + .build({})
11 + .build({});
12 + };
13 + return <Stringify x={x} fn={fn} />;
14 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md new
+60
@@ -0,0 +1,60 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({a, b}) {
6 + const y = {a};
7 + const x = {b};
8 + const f = () => {
9 + let z = null;
10 + while (z == null) {
11 + z = x;
12 + }
13 + // z is a phi with a backedge, and we don't realize it could be x,
14 + // and therefore fail to record a Capture x <- y effect for this
15 + // function expression
16 + z.y = y;
17 + };
18 + f();
19 + mutate(x);
20 + return <div>{x}</div>;
21 +}
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime";
29 +function Component(t0) {
30 + const $ = _c(3);
31 + const { a, b } = t0;
32 + let t1;
33 + if ($[0] !== a || $[1] !== b) {
34 + const y = { a };
35 + const x = { b };
36 + const f = () => {
37 + let z = null;
38 + while (z == null) {
39 + z = x;
40 + }
41 +
42 + z.y = y;
43 + };
44 +
45 + f();
46 + mutate(x);
47 + t1 = <div>{x}</div>;
48 + $[0] = a;
49 + $[1] = b;
50 + $[2] = t1;
51 + } else {
52 + t1 = $[2];
53 + }
54 + return t1;
55 +}
56 +
57 +```
58 +
59 +### Eval output
60 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js new
+17
@@ -0,0 +1,17 @@
1 +function Component({a, b}) {
2 + const y = {a};
3 + const x = {b};
4 + const f = () => {
5 + let z = null;
6 + while (z == null) {
7 + z = x;
8 + }
9 + // z is a phi with a backedge, and we don't realize it could be x,
10 + // and therefore fail to record a Capture x <- y effect for this
11 + // function expression
12 + z.y = y;
13 + };
14 + f();
15 + mutate(x);
16 + return <div>{x}</div>;
17 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md new
+74
@@ -0,0 +1,74 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel:true
6 +
7 +export const App = () => {
8 + const [selected, setSelected] = useState(new Set<string>());
9 + const onSelectedChange = (value: string) => {
10 + const newSelected = new Set(selected);
11 + if (newSelected.has(value)) {
12 + // This should not count as a mutation of `selected`
13 + newSelected.delete(value);
14 + } else {
15 + // This should not count as a mutation of `selected`
16 + newSelected.add(value);
17 + }
18 + setSelected(newSelected);
19 + };
20 +
21 + return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true
30 +
31 +export const App = () => {
32 + const $ = _c(6);
33 + let t0;
34 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35 + t0 = new Set();
36 + $[0] = t0;
37 + } else {
38 + t0 = $[0];
39 + }
40 + const [selected, setSelected] = useState(t0);
41 + let t1;
42 + if ($[1] !== selected) {
43 + t1 = (value) => {
44 + const newSelected = new Set(selected);
45 + if (newSelected.has(value)) {
46 + newSelected.delete(value);
47 + } else {
48 + newSelected.add(value);
49 + }
50 +
51 + setSelected(newSelected);
52 + };
53 + $[1] = selected;
54 + $[2] = t1;
55 + } else {
56 + t1 = $[2];
57 + }
58 + const onSelectedChange = t1;
59 + let t2;
60 + if ($[3] !== onSelectedChange || $[4] !== selected) {
61 + t2 = <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
62 + $[3] = onSelectedChange;
63 + $[4] = selected;
64 + $[5] = t2;
65 + } else {
66 + t2 = $[5];
67 + }
68 + return t2;
69 +};
70 +
71 +```
72 +
73 +### Eval output
74 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js new
+18
@@ -0,0 +1,18 @@
1 +// @enableNewMutationAliasingModel:true
2 +
3 +export const App = () => {
4 + const [selected, setSelected] = useState(new Set<string>());
5 + const onSelectedChange = (value: string) => {
6 + const newSelected = new Set(selected);
7 + if (newSelected.has(value)) {
8 + // This should not count as a mutation of `selected`
9 + newSelected.delete(value);
10 + } else {
11 + // This should not count as a mutation of `selected`
12 + newSelected.add(value);
13 + }
14 + setSelected(newSelected);
15 + };
16 +
17 + return <Stringify selected={selected} onSelectedChange={onSelectedChange} />;
18 +};