@samitouri / QOS-React-1 / commits / 12483a119b

[compiler] Fix for edge cases of mutation of potentially frozen values (#33984)

Fixes two related cases of mutation of potentially frozen values. The first is method calls on frozen values. Previously, we modeled unknown function calls as potentially aliasing their receiver+args into the return value. If the receiver or argument were known to be frozen, then we would downgrade the `Alias` effect into an `ImmutableCapture`. However, within a function expression it's possible to call a function using a frozen value as an argument (that gets `Alias`-ed into the return) but where we don't have the context locally to know that the value is frozen. This results in cases like this: ```js const frozen = useContext(...); useEffect(() => { frozen.method().property = true; ^^^^^^^^^^^^^^^^^^^^^^^^ cannot mutate frozen value }, [...]); ``` Within the function we would infer: ``` t0 = MethodCall ... Create t0 = mutable Alias t0 <- frozen t1 = PropertyStore ... Mutate t0 ``` And then transitively infer the function expression as having a `Mutate 'frozen'` effect, which when evaluated against the outer context (`frozen` is frozen) is an error. The fix is to model unknown function calls as _maybe_ aliasing their receiver/args in the return, and then considering mutations of a maybe-aliased value to only be a conditional mutation of the source: ``` t0 = MethodCall ... Create t0 = mutable MaybeAlias t0 <- frozen // maybe alias now t1 = PropertyStore ... Mutate t0 ``` Then, the `Mutate t0` turns into a `MutateConditional 'frozen'`, which just gets ignored when we process the outer context. The second, related fix is for known mutation of phis that may be a frozen value. The previous inference model correctly recorded these as errors, the new model does not. We now correctly report a validation error for this case in the new model. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33984). * #33993 * #33991 * __->__ #33984

Joseph Savona committed Jul 25, 2025 at 10:07 UTC 12483a119bf21ab69f9837f65b6bed2ba55cb73e
17 files changed +483 -106
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+4 -1
@@ -943,7 +943,10 @@ export function printAliasingEffect(effect: AliasingEffect): string {
943 return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
944 }
945 case 'Alias': {
946 - return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
946 + return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
947 + }
948 + case 'MaybeAlias': {
949 + return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
950 }
951 case 'Capture': {
952 return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts
+19 -1
@@ -90,6 +90,23 @@ export type AliasingEffect =
90 * c could be mutating a.
91 */
92 | {kind: 'Alias'; from: Place; into: Place}
93 +
94 + /**
95 + * Indicates the potential for information flow from `from` to `into`. This is used for a specific
96 + * case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
97 + * consider several possibilities (which may depend on the arguments):
98 + * - foo(x) returns a new mutable value that does not capture any information from x.
99 + * - foo(x) returns a new mutable value that *does* capture information from x.
100 + * - foo(x) returns x itself, ie foo is the identity function
101 + *
102 + * The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
103 + * return b or c depending on the value of a.
104 + *
105 + * To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
106 + * Any mutations that flow through this relationship automatically become conditional.
107 + */
108 + | {kind: 'MaybeAlias'; from: Place; into: Place}
109 +
110 /**
111 * Records direct assignment: `into = from`.
112 */
@@ -183,7 +200,8 @@ export function hashEffect(effect: AliasingEffect): string {
200 case 'ImmutableCapture':
201 case 'Assign':
202 case 'Alias':
186 - case 'Capture': {
203 + case 'Capture':
204 + case 'MaybeAlias': {
205 return [
206 effect.kind,
207 effect.from.identifier.id,
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+2 -1
@@ -85,7 +85,8 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
85 case 'Assign':
86 case 'Alias':
87 case 'Capture':
88 - case 'CreateFrom': {
88 + case 'CreateFrom':
89 + case 'MaybeAlias': {
90 capturedOrMutated.add(effect.from.identifier.id);
91 break;
92 }
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts
+4 -2
@@ -691,6 +691,7 @@ function applyEffect(
691 }
692 break;
693 }
694 + case 'MaybeAlias':
695 case 'Alias':
696 case 'Capture': {
697 CompilerError.invariant(
@@ -955,7 +956,7 @@ function applyEffect(
956 context,
957 state,
958 // OK: recording information flow
958 - {kind: 'Alias', from: operand, into: effect.into},
959 + {kind: 'MaybeAlias', from: operand, into: effect.into},
960 initialized,
961 effects,
962 );
@@ -1323,7 +1324,7 @@ class InferenceState {
1324 return 'mutate-global';
1325 }
1326 case ValueKind.MaybeFrozen: {
1326 - return 'none';
1327 + return 'mutate-frozen';
1328 }
1329 default: {
1330 assertExhaustive(kind, `Unexpected kind ${kind}`);
@@ -2376,6 +2377,7 @@ function computeEffectsForSignature(
2377 // Apply substitutions
2378 for (const effect of signature.effects) {
2379 switch (effect.kind) {
2380 + case 'MaybeAlias':
2381 case 'Assign':
2382 case 'ImmutableCapture':
2383 case 'Alias':
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts
+63 -12
@@ -160,6 +160,8 @@ export function inferMutationAliasingRanges(
160 state.assign(index++, effect.from, effect.into);
161 } else if (effect.kind === 'Alias') {
162 state.assign(index++, effect.from, effect.into);
163 + } else if (effect.kind === 'MaybeAlias') {
164 + state.maybeAlias(index++, effect.from, effect.into);
165 } else if (effect.kind === 'Capture') {
166 state.capture(index++, effect.from, effect.into);
167 } else if (
@@ -346,7 +348,8 @@ export function inferMutationAliasingRanges(
348 case 'Assign':
349 case 'Alias':
350 case 'Capture':
349 - case 'CreateFrom': {
351 + case 'CreateFrom':
352 + case 'MaybeAlias': {
353 const isMutatedOrReassigned =
354 effect.into.identifier.mutableRange.end > instr.id;
355 if (isMutatedOrReassigned) {
@@ -567,7 +570,12 @@ type Node = {
570 createdFrom: Map<Identifier, number>;
571 captures: Map<Identifier, number>;
572 aliases: Map<Identifier, number>;
570 - edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
573 + maybeAliases: Map<Identifier, number>;
574 + edges: Array<{
575 + index: number;
576 + node: Identifier;
577 + kind: 'capture' | 'alias' | 'maybeAlias';
578 + }>;
579 transitive: {kind: MutationKind; loc: SourceLocation} | null;
580 local: {kind: MutationKind; loc: SourceLocation} | null;
581 lastMutated: number;
@@ -585,6 +593,7 @@ class AliasingState {
593 createdFrom: new Map(),
594 captures: new Map(),
595 aliases: new Map(),
596 + maybeAliases: new Map(),
597 edges: [],
598 transitive: null,
599 local: null,
@@ -630,6 +639,18 @@ class AliasingState {
639 }
640 }
641
642 + maybeAlias(index: number, from: Place, into: Place): void {
643 + const fromNode = this.nodes.get(from.identifier);
644 + const toNode = this.nodes.get(into.identifier);
645 + if (fromNode == null || toNode == null) {
646 + return;
647 + }
648 + fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
649 + if (!toNode.maybeAliases.has(from.identifier)) {
650 + toNode.maybeAliases.set(from.identifier, index);
651 + }
652 + }
653 +
654 render(index: number, start: Identifier, errors: CompilerError): void {
655 const seen = new Set<Identifier>();
656 const queue: Array<Identifier> = [start];
@@ -673,22 +694,24 @@ class AliasingState {
694 // Null is used for simulated mutations
695 end: InstructionId | null,
696 transitive: boolean,
676 - kind: MutationKind,
697 + startKind: MutationKind,
698 loc: SourceLocation,
699 errors: CompilerError,
700 ): void {
680 - const seen = new Set<Identifier>();
701 + const seen = new Map<Identifier, MutationKind>();
702 const queue: Array<{
703 place: Identifier;
704 transitive: boolean;
705 direction: 'backwards' | 'forwards';
685 - }> = [{place: start, transitive, direction: 'backwards'}];
706 + kind: MutationKind;
707 + }> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
708 while (queue.length !== 0) {
687 - const {place: current, transitive, direction} = queue.pop()!;
688 - if (seen.has(current)) {
709 + const {place: current, transitive, direction, kind} = queue.pop()!;
710 + const previousKind = seen.get(current);
711 + if (previousKind != null && previousKind >= kind) {
712 continue;
713 }
691 - seen.add(current);
714 + seen.set(current, kind);
715 const node = this.nodes.get(current);
716 if (node == null) {
717 continue;
@@ -724,13 +747,18 @@ class AliasingState {
747 if (edge.index >= index) {
748 break;
749 }
727 - queue.push({place: edge.node, transitive, direction: 'forwards'});
750 + queue.push({place: edge.node, transitive, direction: 'forwards', kind});
751 }
752 for (const [alias, when] of node.createdFrom) {
753 if (when >= index) {
754 continue;
755 }
733 - queue.push({place: alias, transitive: true, direction: 'backwards'});
756 + queue.push({
757 + place: alias,
758 + transitive: true,
759 + direction: 'backwards',
760 + kind,
761 + });
762 }
763 if (direction === 'backwards' || node.value.kind !== 'Phi') {
764 /**
@@ -747,7 +775,25 @@ class AliasingState {
775 if (when >= index) {
776 continue;
777 }
750 - queue.push({place: alias, transitive, direction: 'backwards'});
778 + queue.push({place: alias, transitive, direction: 'backwards', kind});
779 + }
780 + /**
781 + * MaybeAlias indicates potential data flow from unknown function calls,
782 + * so we downgrade mutations through these aliases to consider them
783 + * conditional. This means we'll consider them for mutation *range*
784 + * purposes but not report validation errors for mutations, since
785 + * we aren't sure that the `from` value could actually be aliased.
786 + */
787 + for (const [alias, when] of node.maybeAliases) {
788 + if (when >= index) {
789 + continue;
790 + }
791 + queue.push({
792 + place: alias,
793 + transitive,
794 + direction: 'backwards',
795 + kind: MutationKind.Conditional,
796 + });
797 }
798 }
799 /**
@@ -758,7 +804,12 @@ class AliasingState {
804 if (when >= index) {
805 continue;
806 }
761 - queue.push({place: capture, transitive, direction: 'backwards'});
807 + queue.push({
808 + place: capture,
809 + transitive,
810 + direction: 'backwards',
811 + kind,
812 + });
813 }
814 }
815 }
compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md
+15
@@ -153,6 +153,10 @@ This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes tha
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`).
@@ -347,6 +351,17 @@ 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:
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-mutate-phi-which-could-be-frozen.expect.md new
+39
@@ -0,0 +1,39 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useHook} from 'shared-runtime';
6 +
7 +function Component(props) {
8 + const frozen = useHook();
9 + let x;
10 + if (props.cond) {
11 + x = frozen;
12 + } else {
13 + x = {};
14 + }
15 + x.property = true;
16 +}
17 +
18 +```
19 +
20 +
21 +## Error
22 +
23 +```
24 +Found 1 error:
25 +
26 +Error: This value cannot be modified
27 +
28 +Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
29 +
30 +error.invalid-mutate-phi-which-could-be-frozen.ts:11:2
31 + 9 | x = {};
32 + 10 | }
33 +> 11 | x.property = true;
34 + | ^ value cannot be modified
35 + 12 | }
36 + 13 |
37 +```
38 +
39 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-mutate-phi-which-could-be-frozen.js new
+12
@@ -0,0 +1,12 @@
1 +import {useHook} from 'shared-runtime';
2 +
3 +function Component(props) {
4 + const frozen = useHook();
5 + let x;
6 + if (props.cond) {
7 + x = frozen;
8 + } else {
9 + x = {};
10 + }
11 + x.property = true;
12 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-for-loop-with-context-variable-iterator.expect.md new
+61
@@ -0,0 +1,61 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, useIdentity} from 'shared-runtime';
6 +
7 +function Component() {
8 + const data = useIdentity(
9 + new Map([
10 + [0, 'value0'],
11 + [1, 'value1'],
12 + ])
13 + );
14 + const items = [];
15 + // NOTE: `i` is a context variable because it's reassigned and also referenced
16 + // within a closure, the `onClick` handler of each item
17 + // TODO: for loops create a unique environment on each iteration, which means
18 + // that if the iteration variable is only updated in the updater, the variable
19 + // is effectively const within the body and the "update" acts more like
20 + // a re-initialization than a reassignment.
21 + // Until we model this "new environment" semantic, we allow this case to error
22 + for (let i = MIN; i <= MAX; i += INCREMENT) {
23 + items.push(
24 + <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
25 + );
26 + }
27 + return <>{items}</>;
28 +}
29 +
30 +const MIN = 0;
31 +const MAX = 3;
32 +const INCREMENT = 1;
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + params: [],
36 + fn: Component,
37 +};
38 +
39 +```
40 +
41 +
42 +## Error
43 +
44 +```
45 +Found 1 error:
46 +
47 +Error: This value cannot be modified
48 +
49 +Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
50 +
51 +error.todo-for-loop-with-context-variable-iterator.ts:18:30
52 + 16 | // a re-initialization than a reassignment.
53 + 17 | // Until we model this "new environment" semantic, we allow this case to error
54 +> 18 | for (let i = MIN; i <= MAX; i += INCREMENT) {
55 + | ^ `i` cannot be modified
56 + 19 | items.push(
57 + 20 | <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
58 + 21 | );
59 +```
60 +
61 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-for-loop-with-context-variable-iterator.js renamed
+5
@@ -10,6 +10,11 @@ function Component() {
10 const items = [];
11 // NOTE: `i` is a context variable because it's reassigned and also referenced
12 // within a closure, the `onClick` handler of each item
13 + // TODO: for loops create a unique environment on each iteration, which means
14 + // that if the iteration variable is only updated in the updater, the variable
15 + // is effectively const within the body and the "update" acts more like
16 + // a re-initialization than a reassignment.
17 + // Until we model this "new environment" semantic, we allow this case to error
18 for (let i = MIN; i <= MAX; i += INCREMENT) {
19 items.push(
20 <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md deleted
-89
@@ -1,89 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {Stringify, useIdentity} from 'shared-runtime';
6 -
7 -function Component() {
8 - const data = useIdentity(
9 - new Map([
10 - [0, 'value0'],
11 - [1, 'value1'],
12 - ])
13 - );
14 - const items = [];
15 - // NOTE: `i` is a context variable because it's reassigned and also referenced
16 - // within a closure, the `onClick` handler of each item
17 - for (let i = MIN; i <= MAX; i += INCREMENT) {
18 - items.push(
19 - <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
20 - );
21 - }
22 - return <>{items}</>;
23 -}
24 -
25 -const MIN = 0;
26 -const MAX = 3;
27 -const INCREMENT = 1;
28 -
29 -export const FIXTURE_ENTRYPOINT = {
30 - params: [],
31 - fn: Component,
32 -};
33 -
34 -```
35 -
36 -## Code
37 -
38 -```javascript
39 -import { c as _c } from "react/compiler-runtime";
40 -import { Stringify, useIdentity } from "shared-runtime";
41 -
42 -function Component() {
43 - const $ = _c(3);
44 - let t0;
45 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
46 - t0 = new Map([
47 - [0, "value0"],
48 - [1, "value1"],
49 - ]);
50 - $[0] = t0;
51 - } else {
52 - t0 = $[0];
53 - }
54 - const data = useIdentity(t0);
55 - let t1;
56 - if ($[1] !== data) {
57 - const items = [];
58 - for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
59 - items.push(
60 - <Stringify
61 - key={i}
62 - onClick={() => data.get(i)}
63 - shouldInvokeFns={true}
64 - />,
65 - );
66 - }
67 -
68 - t1 = <>{items}</>;
69 - $[1] = data;
70 - $[2] = t1;
71 - } else {
72 - t1 = $[2];
73 - }
74 - return t1;
75 -}
76 -
77 -const MIN = 0;
78 -const MAX = 3;
79 -const INCREMENT = 1;
80 -
81 -export const FIXTURE_ENTRYPOINT = {
82 - params: [],
83 - fn: Component,
84 -};
85 -
86 -```
87 -
88 -### Eval output
89 -(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-function-call-with-frozen-argument-in-function-expression.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {identity, makeObject_Primitives, Stringify} from 'shared-runtime';
6 +
7 +function Example(props) {
8 + const object = props.object;
9 + const f = () => {
10 + // The argument maybe-aliases into the return
11 + const obj = identity(object);
12 + obj.property = props.value;
13 + return obj;
14 + };
15 + const obj = f();
16 + return <Stringify obj={obj} />;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Example,
21 + params: [{object: makeObject_Primitives(), value: 42}],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime";
30 +import { identity, makeObject_Primitives, Stringify } from "shared-runtime";
31 +
32 +function Example(props) {
33 + const $ = _c(7);
34 + const object = props.object;
35 + let t0;
36 + if ($[0] !== object || $[1] !== props.value) {
37 + t0 = () => {
38 + const obj = identity(object);
39 + obj.property = props.value;
40 + return obj;
41 + };
42 + $[0] = object;
43 + $[1] = props.value;
44 + $[2] = t0;
45 + } else {
46 + t0 = $[2];
47 + }
48 + const f = t0;
49 + let t1;
50 + if ($[3] !== f) {
51 + t1 = f();
52 + $[3] = f;
53 + $[4] = t1;
54 + } else {
55 + t1 = $[4];
56 + }
57 + const obj_0 = t1;
58 + let t2;
59 + if ($[5] !== obj_0) {
60 + t2 = <Stringify obj={obj_0} />;
61 + $[5] = obj_0;
62 + $[6] = t2;
63 + } else {
64 + t2 = $[6];
65 + }
66 + return t2;
67 +}
68 +
69 +export const FIXTURE_ENTRYPOINT = {
70 + fn: Example,
71 + params: [{ object: makeObject_Primitives(), value: 42 }],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-function-call-with-frozen-argument-in-function-expression.js new
+18
@@ -0,0 +1,18 @@
1 +import {identity, makeObject_Primitives, Stringify} from 'shared-runtime';
2 +
3 +function Example(props) {
4 + const object = props.object;
5 + const f = () => {
6 + // The argument maybe-aliases into the return
7 + const obj = identity(object);
8 + obj.property = props.value;
9 + return obj;
10 + };
11 + const obj = f();
12 + return <Stringify obj={obj} />;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Example,
17 + params: [{object: makeObject_Primitives(), value: 42}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-method-call-on-frozen-value-in-function-expression.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {makeObject_Primitives, Stringify} from 'shared-runtime';
6 +
7 +function Example(props) {
8 + const object = props.object;
9 + const f = () => {
10 + // The receiver maybe-aliases into the return
11 + const obj = object.makeObject();
12 + obj.property = props.value;
13 + return obj;
14 + };
15 + const obj = f();
16 + return <Stringify obj={obj} />;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Example,
21 + params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime";
30 +import { makeObject_Primitives, Stringify } from "shared-runtime";
31 +
32 +function Example(props) {
33 + const $ = _c(7);
34 + const object = props.object;
35 + let t0;
36 + if ($[0] !== object || $[1] !== props.value) {
37 + t0 = () => {
38 + const obj = object.makeObject();
39 + obj.property = props.value;
40 + return obj;
41 + };
42 + $[0] = object;
43 + $[1] = props.value;
44 + $[2] = t0;
45 + } else {
46 + t0 = $[2];
47 + }
48 + const f = t0;
49 + let t1;
50 + if ($[3] !== f) {
51 + t1 = f();
52 + $[3] = f;
53 + $[4] = t1;
54 + } else {
55 + t1 = $[4];
56 + }
57 + const obj_0 = t1;
58 + let t2;
59 + if ($[5] !== obj_0) {
60 + t2 = <Stringify obj={obj_0} />;
61 + $[5] = obj_0;
62 + $[6] = t2;
63 + } else {
64 + t2 = $[6];
65 + }
66 + return t2;
67 +}
68 +
69 +export const FIXTURE_ENTRYPOINT = {
70 + fn: Example,
71 + params: [{ object: { makeObject: makeObject_Primitives }, value: 42 }],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-method-call-on-frozen-value-in-function-expression.js new
+18
@@ -0,0 +1,18 @@
1 +import {makeObject_Primitives, Stringify} from 'shared-runtime';
2 +
3 +function Example(props) {
4 + const object = props.object;
5 + const f = () => {
6 + // The receiver maybe-aliases into the return
7 + const obj = object.makeObject();
8 + obj.property = props.value;
9 + return obj;
10 + };
11 + const obj = f();
12 + return <Stringify obj={obj} />;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Example,
17 + params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-method-call-on-frozen-value-is-allowed.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {makeObject_Primitives, Stringify} from 'shared-runtime';
6 +
7 +function Example(props) {
8 + const obj = props.object.makeObject();
9 + obj.property = props.value;
10 + return <Stringify obj={obj} />;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Example,
15 + params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
16 +};
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { c as _c } from "react/compiler-runtime";
24 +import { makeObject_Primitives, Stringify } from "shared-runtime";
25 +
26 +function Example(props) {
27 + const $ = _c(5);
28 + let obj;
29 + if ($[0] !== props.object || $[1] !== props.value) {
30 + obj = props.object.makeObject();
31 + obj.property = props.value;
32 + $[0] = props.object;
33 + $[1] = props.value;
34 + $[2] = obj;
35 + } else {
36 + obj = $[2];
37 + }
38 + let t0;
39 + if ($[3] !== obj) {
40 + t0 = <Stringify obj={obj} />;
41 + $[3] = obj;
42 + $[4] = t0;
43 + } else {
44 + t0 = $[4];
45 + }
46 + return t0;
47 +}
48 +
49 +export const FIXTURE_ENTRYPOINT = {
50 + fn: Example,
51 + params: [{ object: { makeObject: makeObject_Primitives }, value: 42 }],
52 +};
53 +
54 +```
55 +
56 +### Eval output
57 +(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-mutate-result-of-method-call-on-frozen-value-is-allowed.js new
+12
@@ -0,0 +1,12 @@
1 +import {makeObject_Primitives, Stringify} from 'shared-runtime';
2 +
3 +function Example(props) {
4 + const obj = props.object.makeObject();
5 + obj.property = props.value;
6 + return <Stringify obj={obj} />;
7 +}
8 +
9 +export const FIXTURE_ENTRYPOINT = {
10 + fn: Example,
11 + params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
12 +};