@samitouri / QOS-React / commits / febc09b480

[compiler][fix] mutableOnlyIfOperandsAreMutable does not apply when operands are globals (#32695)

Globals, module locals, and other locally defined functions may mutate their arguments. See test fixtures for details --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32695). * #32698 * #32697 * #32696 * __->__ #32695

mofeiZ committed Mar 23, 2025 at 23:07 UTC febc09b480903bb803455dc38dc130007d3a2e91
12 files changed +709 -30
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+98 -12
@@ -11,6 +11,7 @@ import {
11 BuiltInArrayId,
12 BuiltInFireId,
13 BuiltInMixedReadonlyId,
14 + BuiltInObjectId,
15 BuiltInUseActionStateId,
16 BuiltInUseContextHookId,
17 BuiltInUseEffectHookId,
@@ -45,21 +46,17 @@ export const DEFAULT_SHAPES: ShapeRegistry = new Map(BUILTIN_SHAPES);
46
47 // Hack until we add ObjectShapes for all globals
48 const UNTYPED_GLOBALS: Set<string> = new Set([
48 - 'String',
49 'Object',
50 'Function',
51 - 'Number',
51 'RegExp',
52 'Date',
53 'Error',
55 - 'Function',
54 'TypeError',
55 'RangeError',
56 'ReferenceError',
57 'SyntaxError',
58 'URIError',
59 'EvalError',
62 - 'Boolean',
60 'DataView',
61 'Float32Array',
62 'Float64Array',
@@ -75,16 +72,8 @@ const UNTYPED_GLOBALS: Set<string> = new Set([
72 'Uint32Array',
73 'ArrayBuffer',
74 'JSON',
78 - 'parseFloat',
79 - 'parseInt',
75 'console',
81 - 'isNaN',
76 'eval',
83 - 'isFinite',
84 - 'encodeURI',
85 - 'decodeURI',
86 - 'encodeURIComponent',
87 - 'decodeURIComponent',
77 ]);
78
79 const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
@@ -101,6 +90,23 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
90 returnValueKind: ValueKind.Mutable,
91 }),
92 ],
93 + [
94 + /**
95 + * Object.fromEntries(iterable)
96 + * iterable: An iterable, such as an Array or Map, containing a list of
97 + * objects. Each object should have two properties.
98 + * Returns a new object whose properties are given by the entries of the
99 + * iterable.
100 + */
101 + 'fromEntries',
102 + addFunction(DEFAULT_SHAPES, [], {
103 + positionalParams: [Effect.ConditionallyMutate],
104 + restParam: null,
105 + returnType: {kind: 'Object', shapeId: BuiltInObjectId},
106 + calleeEffect: Effect.Read,
107 + returnValueKind: ValueKind.Mutable,
108 + }),
109 + ],
110 ]),
111 ],
112 [
@@ -372,6 +378,86 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
378 returnValueKind: ValueKind.Primitive,
379 }),
380 ],
381 + [
382 + 'parseInt',
383 + addFunction(DEFAULT_SHAPES, [], {
384 + positionalParams: [],
385 + restParam: Effect.Read,
386 + returnType: {kind: 'Primitive'},
387 + calleeEffect: Effect.Read,
388 + returnValueKind: ValueKind.Primitive,
389 + }),
390 + ],
391 + [
392 + 'parseFloat',
393 + addFunction(DEFAULT_SHAPES, [], {
394 + positionalParams: [],
395 + restParam: Effect.Read,
396 + returnType: {kind: 'Primitive'},
397 + calleeEffect: Effect.Read,
398 + returnValueKind: ValueKind.Primitive,
399 + }),
400 + ],
401 + [
402 + 'isNaN',
403 + addFunction(DEFAULT_SHAPES, [], {
404 + positionalParams: [],
405 + restParam: Effect.Read,
406 + returnType: {kind: 'Primitive'},
407 + calleeEffect: Effect.Read,
408 + returnValueKind: ValueKind.Primitive,
409 + }),
410 + ],
411 + [
412 + 'isFinite',
413 + addFunction(DEFAULT_SHAPES, [], {
414 + positionalParams: [],
415 + restParam: Effect.Read,
416 + returnType: {kind: 'Primitive'},
417 + calleeEffect: Effect.Read,
418 + returnValueKind: ValueKind.Primitive,
419 + }),
420 + ],
421 + [
422 + 'encodeURI',
423 + addFunction(DEFAULT_SHAPES, [], {
424 + positionalParams: [],
425 + restParam: Effect.Read,
426 + returnType: {kind: 'Primitive'},
427 + calleeEffect: Effect.Read,
428 + returnValueKind: ValueKind.Primitive,
429 + }),
430 + ],
431 + [
432 + 'encodeURIComponent',
433 + addFunction(DEFAULT_SHAPES, [], {
434 + positionalParams: [],
435 + restParam: Effect.Read,
436 + returnType: {kind: 'Primitive'},
437 + calleeEffect: Effect.Read,
438 + returnValueKind: ValueKind.Primitive,
439 + }),
440 + ],
441 + [
442 + 'decodeURI',
443 + addFunction(DEFAULT_SHAPES, [], {
444 + positionalParams: [],
445 + restParam: Effect.Read,
446 + returnType: {kind: 'Primitive'},
447 + calleeEffect: Effect.Read,
448 + returnValueKind: ValueKind.Primitive,
449 + }),
450 + ],
451 + [
452 + 'decodeURIComponent',
453 + addFunction(DEFAULT_SHAPES, [], {
454 + positionalParams: [],
455 + restParam: Effect.Read,
456 + returnType: {kind: 'Primitive'},
457 + calleeEffect: Effect.Read,
458 + returnValueKind: ValueKind.Primitive,
459 + }),
460 + ],
461 // TODO: rest of Global objects
462 ];
463
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+47 -7
@@ -251,7 +251,7 @@ type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
251
252 // Maintains a mapping of top-level variables to the kind of value they hold
253 class InferenceState {
254 - #env: Environment;
254 + env: Environment;
255
256 // The kind of each value, based on its allocation site
257 #values: Map<InstructionValue, AbstractValue>;
@@ -267,7 +267,7 @@ class InferenceState {
267 values: Map<InstructionValue, AbstractValue>,
268 variables: Map<IdentifierId, Set<InstructionValue>>,
269 ) {
270 - this.#env = env;
270 + this.env = env;
271 this.#values = values;
272 this.#variables = variables;
273 }
@@ -409,8 +409,8 @@ class InferenceState {
409 });
410 if (
411 value.kind === 'FunctionExpression' &&
412 - (this.#env.config.enablePreserveExistingMemoizationGuarantees ||
413 - this.#env.config.enableTransitivelyFreezeFunctionExpressions)
412 + (this.env.config.enablePreserveExistingMemoizationGuarantees ||
413 + this.env.config.enableTransitivelyFreezeFunctionExpressions)
414 ) {
415 for (const operand of value.loweredFunc.func.context) {
416 const operandValues = this.#variables.get(operand.identifier.id);
@@ -590,7 +590,7 @@ class InferenceState {
590 return null;
591 } else {
592 return new InferenceState(
593 - this.#env,
593 + this.env,
594 nextValues ?? new Map(this.#values),
595 nextVariables ?? new Map(this.#variables),
596 );
@@ -604,7 +604,7 @@ class InferenceState {
604 */
605 clone(): InferenceState {
606 return new InferenceState(
607 - this.#env,
607 + this.env,
608 new Map(this.#values),
609 new Map(this.#variables),
610 );
@@ -2012,6 +2012,32 @@ export function getFunctionEffects(
2012 return results;
2013 }
2014
2015 +export function isKnownMutableEffect(effect: Effect): boolean {
2016 + switch (effect) {
2017 + case Effect.Store:
2018 + case Effect.ConditionallyMutate:
2019 + case Effect.Mutate: {
2020 + return true;
2021 + }
2022 +
2023 + case Effect.Unknown: {
2024 + CompilerError.invariant(false, {
2025 + reason: 'Unexpected unknown effect',
2026 + description: null,
2027 + loc: GeneratedSource,
2028 + suggestions: null,
2029 + });
2030 + }
2031 + case Effect.Read:
2032 + case Effect.Capture:
2033 + case Effect.Freeze: {
2034 + return false;
2035 + }
2036 + default: {
2037 + assertExhaustive(effect, `Unexpected effect \`${effect}\``);
2038 + }
2039 + }
2040 +}
2041 /**
2042 * Returns true if all of the arguments are both non-mutable (immutable or frozen)
2043 * _and_ are not functions which might mutate their arguments. Note that function
@@ -2023,10 +2049,20 @@ function areArgumentsImmutableAndNonMutating(
2049 args: MethodCall['args'],
2050 ): boolean {
2051 for (const arg of args) {
2052 + if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
2053 + const fnShape = state.env.getFunctionSignature(arg.identifier.type);
2054 + if (fnShape != null) {
2055 + return (
2056 + !fnShape.positionalParams.some(isKnownMutableEffect) &&
2057 + (fnShape.restParam == null ||
2058 + !isKnownMutableEffect(fnShape.restParam))
2059 + );
2060 + }
2061 + }
2062 const place = arg.kind === 'Identifier' ? arg : arg.place;
2063 +
2064 const kind = state.kind(place).kind;
2065 switch (kind) {
2029 - case ValueKind.Global:
2066 case ValueKind.Primitive:
2067 case ValueKind.Frozen: {
2068 /*
@@ -2037,6 +2073,10 @@ function areArgumentsImmutableAndNonMutating(
2073 break;
2074 }
2075 default: {
2076 + /**
2077 + * Globals, module locals, and other locally defined functions may
2078 + * mutate their arguments.
2079 + */
2080 return false;
2081 }
2082 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.expect.md
+29 -10
@@ -8,7 +8,12 @@ function Component({value}) {
8 const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
9 useIdentity();
10 const derived = Array.from(arr, mutateAndReturn);
11 - return <Stringify>{derived.at(-1)}</Stringify>;
11 + return (
12 + <Stringify>
13 + {derived.at(0)}
14 + {derived.at(-1)}
15 + </Stringify>
16 + );
17 }
18
19 export const FIXTURE_ENTRYPOINT = {
@@ -26,28 +31,42 @@ import { c as _c } from "react/compiler-runtime";
31 import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
32
33 function Component(t0) {
29 - const $ = _c(4);
34 + const $ = _c(7);
35 const { value } = t0;
36 const arr = [{ value: "foo" }, { value: "bar" }, { value }];
37 useIdentity();
38 const derived = Array.from(arr, mutateAndReturn);
39 let t1;
40 if ($[0] !== derived) {
36 - t1 = derived.at(-1);
41 + t1 = derived.at(0);
42 $[0] = derived;
43 $[1] = t1;
44 } else {
45 t1 = $[1];
46 }
47 let t2;
43 - if ($[2] !== t1) {
44 - t2 = <Stringify>{t1}</Stringify>;
45 - $[2] = t1;
48 + if ($[2] !== derived) {
49 + t2 = derived.at(-1);
50 + $[2] = derived;
51 $[3] = t2;
52 } else {
53 t2 = $[3];
54 }
50 - return t2;
55 + let t3;
56 + if ($[4] !== t1 || $[5] !== t2) {
57 + t3 = (
58 + <Stringify>
59 + {t1}
60 + {t2}
61 + </Stringify>
62 + );
63 + $[4] = t1;
64 + $[5] = t2;
65 + $[6] = t3;
66 + } else {
67 + t3 = $[6];
68 + }
69 + return t3;
70 }
71
72 export const FIXTURE_ENTRYPOINT = {
@@ -59,6 +78,6 @@ export const FIXTURE_ENTRYPOINT = {
78 ```
79
80 ### Eval output
62 -(kind: ok) <div>{"children":{"value":5,"wat0":"joe"}}</div>
63 -<div>{"children":{"value":6,"wat0":"joe"}}</div>
64 -<div>{"children":{"value":6,"wat0":"joe"}}</div>
\ No newline at end of file
81 +(kind: ok) <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
82 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
83 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.js
+6 -1
@@ -4,7 +4,12 @@ function Component({value}) {
4 const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
5 useIdentity();
6 const derived = Array.from(arr, mutateAndReturn);
7 - return <Stringify>{derived.at(-1)}</Stringify>;
7 + return (
8 + <Stringify>
9 + {derived.at(0)}
10 + {derived.at(-1)}
11 + </Stringify>
12 + );
13 }
14
15 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.expect.md new
+113
@@ -0,0 +1,113 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
6 +
7 +/**
8 + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag
9 + * Found differences in evaluator results
10 + * Non-forget (expected):
11 + * (kind: ok)
12 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
13 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
14 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
15 + * Forget:
16 + * (kind: ok)
17 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
18 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
19 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
20 +
21 + */
22 +function Component({value}) {
23 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
24 + useIdentity(null);
25 + const derived = arr.filter(mutateAndReturn);
26 + return (
27 + <Stringify>
28 + {derived.at(0)}
29 + {derived.at(-1)}
30 + </Stringify>
31 + );
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Component,
36 + params: [{value: 5}],
37 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
38 +};
39 +
40 +```
41 +
42 +## Code
43 +
44 +```javascript
45 +import { c as _c } from "react/compiler-runtime";
46 +import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
47 +
48 +/**
49 + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag
50 + * Found differences in evaluator results
51 + * Non-forget (expected):
52 + * (kind: ok)
53 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
54 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
55 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
56 + * Forget:
57 + * (kind: ok)
58 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
59 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
60 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
61 +
62 + */
63 +function Component(t0) {
64 + const $ = _c(7);
65 + const { value } = t0;
66 + const arr = [{ value: "foo" }, { value: "bar" }, { value }];
67 + useIdentity(null);
68 + const derived = arr.filter(mutateAndReturn);
69 + let t1;
70 + if ($[0] !== derived) {
71 + t1 = derived.at(0);
72 + $[0] = derived;
73 + $[1] = t1;
74 + } else {
75 + t1 = $[1];
76 + }
77 + let t2;
78 + if ($[2] !== derived) {
79 + t2 = derived.at(-1);
80 + $[2] = derived;
81 + $[3] = t2;
82 + } else {
83 + t2 = $[3];
84 + }
85 + let t3;
86 + if ($[4] !== t1 || $[5] !== t2) {
87 + t3 = (
88 + <Stringify>
89 + {t1}
90 + {t2}
91 + </Stringify>
92 + );
93 + $[4] = t1;
94 + $[5] = t2;
95 + $[6] = t3;
96 + } else {
97 + t3 = $[6];
98 + }
99 + return t3;
100 +}
101 +
102 +export const FIXTURE_ENTRYPOINT = {
103 + fn: Component,
104 + params: [{ value: 5 }],
105 + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
106 +};
107 +
108 +```
109 +
110 +### Eval output
111 +(kind: ok) <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
112 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
113 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.tsx new
+34
@@ -0,0 +1,34 @@
1 +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
2 +
3 +/**
4 + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag
5 + * Found differences in evaluator results
6 + * Non-forget (expected):
7 + * (kind: ok)
8 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
9 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
10 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
11 + * Forget:
12 + * (kind: ok)
13 + * <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
14 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
15 + * <div>{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}</div>
16 +
17 + */
18 +function Component({value}) {
19 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
20 + useIdentity(null);
21 + const derived = arr.filter(mutateAndReturn);
22 + return (
23 + <Stringify>
24 + {derived.at(0)}
25 + {derived.at(-1)}
26 + </Stringify>
27 + );
28 +}
29 +
30 +export const FIXTURE_ENTRYPOINT = {
31 + fn: Component,
32 + params: [{value: 5}],
33 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
34 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.expect.md new
+118
@@ -0,0 +1,118 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, useIdentity} from 'shared-runtime';
6 +
7 +/**
8 + * Also see repro-array-map-known-mutate-shape, which calls a global function
9 + * that mutates its operands.
10 + */
11 +function Component({value}) {
12 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
13 + useIdentity(null);
14 + const derived = arr.filter(Boolean);
15 + return (
16 + <Stringify>
17 + {derived.at(0)}
18 + {derived.at(-1)}
19 + </Stringify>
20 + );
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{value: 5}],
26 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +import { Stringify, useIdentity } from "shared-runtime";
36 +
37 +/**
38 + * Also see repro-array-map-known-mutate-shape, which calls a global function
39 + * that mutates its operands.
40 + */
41 +function Component(t0) {
42 + const $ = _c(13);
43 + const { value } = t0;
44 + let t1;
45 + let t2;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t1 = { value: "foo" };
48 + t2 = { value: "bar" };
49 + $[0] = t1;
50 + $[1] = t2;
51 + } else {
52 + t1 = $[0];
53 + t2 = $[1];
54 + }
55 + let t3;
56 + if ($[2] !== value) {
57 + t3 = [t1, t2, { value }];
58 + $[2] = value;
59 + $[3] = t3;
60 + } else {
61 + t3 = $[3];
62 + }
63 + const arr = t3;
64 + useIdentity(null);
65 + let t4;
66 + if ($[4] !== arr) {
67 + t4 = arr.filter(Boolean);
68 + $[4] = arr;
69 + $[5] = t4;
70 + } else {
71 + t4 = $[5];
72 + }
73 + const derived = t4;
74 + let t5;
75 + if ($[6] !== derived) {
76 + t5 = derived.at(0);
77 + $[6] = derived;
78 + $[7] = t5;
79 + } else {
80 + t5 = $[7];
81 + }
82 + let t6;
83 + if ($[8] !== derived) {
84 + t6 = derived.at(-1);
85 + $[8] = derived;
86 + $[9] = t6;
87 + } else {
88 + t6 = $[9];
89 + }
90 + let t7;
91 + if ($[10] !== t5 || $[11] !== t6) {
92 + t7 = (
93 + <Stringify>
94 + {t5}
95 + {t6}
96 + </Stringify>
97 + );
98 + $[10] = t5;
99 + $[11] = t6;
100 + $[12] = t7;
101 + } else {
102 + t7 = $[12];
103 + }
104 + return t7;
105 +}
106 +
107 +export const FIXTURE_ENTRYPOINT = {
108 + fn: Component,
109 + params: [{ value: 5 }],
110 + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
111 +};
112 +
113 +```
114 +
115 +### Eval output
116 +(kind: ok) <div>{"children":[{"value":"foo"},{"value":5}]}</div>
117 +<div>{"children":[{"value":"foo"},{"value":6}]}</div>
118 +<div>{"children":[{"value":"foo"},{"value":6}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.tsx new
+23
@@ -0,0 +1,23 @@
1 +import {Stringify, useIdentity} from 'shared-runtime';
2 +
3 +/**
4 + * Also see repro-array-map-known-mutate-shape, which calls a global function
5 + * that mutates its operands.
6 + */
7 +function Component({value}) {
8 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
9 + useIdentity(null);
10 + const derived = arr.filter(Boolean);
11 + return (
12 + <Stringify>
13 + {derived.at(0)}
14 + {derived.at(-1)}
15 + </Stringify>
16 + );
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{value: 5}],
22 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.expect.md new
+91
@@ -0,0 +1,91 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
6 +
7 +/**
8 + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any
9 + * function call which captures its callee when applying an operand.
10 + */
11 +function Component({value}) {
12 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
13 + useIdentity(null);
14 + const derived = arr.map(mutateAndReturn);
15 + return (
16 + <Stringify>
17 + {derived.at(0)}
18 + {derived.at(-1)}
19 + </Stringify>
20 + );
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{value: 5}],
26 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
36 +
37 +/**
38 + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any
39 + * function call which captures its callee when applying an operand.
40 + */
41 +function Component(t0) {
42 + const $ = _c(7);
43 + const { value } = t0;
44 + const arr = [{ value: "foo" }, { value: "bar" }, { value }];
45 + useIdentity(null);
46 + const derived = arr.map(mutateAndReturn);
47 + let t1;
48 + if ($[0] !== derived) {
49 + t1 = derived.at(0);
50 + $[0] = derived;
51 + $[1] = t1;
52 + } else {
53 + t1 = $[1];
54 + }
55 + let t2;
56 + if ($[2] !== derived) {
57 + t2 = derived.at(-1);
58 + $[2] = derived;
59 + $[3] = t2;
60 + } else {
61 + t2 = $[3];
62 + }
63 + let t3;
64 + if ($[4] !== t1 || $[5] !== t2) {
65 + t3 = (
66 + <Stringify>
67 + {t1}
68 + {t2}
69 + </Stringify>
70 + );
71 + $[4] = t1;
72 + $[5] = t2;
73 + $[6] = t3;
74 + } else {
75 + t3 = $[6];
76 + }
77 + return t3;
78 +}
79 +
80 +export const FIXTURE_ENTRYPOINT = {
81 + fn: Component,
82 + params: [{ value: 5 }],
83 + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
84 +};
85 +
86 +```
87 +
88 +### Eval output
89 +(kind: ok) <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
90 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
91 +<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.tsx new
+23
@@ -0,0 +1,23 @@
1 +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
2 +
3 +/**
4 + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any
5 + * function call which captures its callee when applying an operand.
6 + */
7 +function Component({value}) {
8 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
9 + useIdentity(null);
10 + const derived = arr.map(mutateAndReturn);
11 + return (
12 + <Stringify>
13 + {derived.at(0)}
14 + {derived.at(-1)}
15 + </Stringify>
16 + );
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{value: 5}],
22 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.expect.md new
+100
@@ -0,0 +1,100 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, useIdentity} from 'shared-runtime';
6 +
7 +/**
8 + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global
9 + * function that does *not* mutate its operands.
10 + */
11 +function Component({value}) {
12 + const arr = [
13 + new Set([['foo', 2]]).values(),
14 + new Set([['bar', 4]]).values(),
15 + [['baz', value]],
16 + ];
17 + useIdentity(null);
18 + const derived = arr.map(Object.fromEntries);
19 + return (
20 + <Stringify>
21 + {derived.at(0)}
22 + {derived.at(-1)}
23 + </Stringify>
24 + );
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: Component,
29 + params: [{value: 5}],
30 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { c as _c } from "react/compiler-runtime";
39 +import { Stringify, useIdentity } from "shared-runtime";
40 +
41 +/**
42 + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global
43 + * function that does *not* mutate its operands.
44 + */
45 +function Component(t0) {
46 + const $ = _c(7);
47 + const { value } = t0;
48 + const arr = [
49 + new Set([["foo", 2]]).values(),
50 + new Set([["bar", 4]]).values(),
51 + [["baz", value]],
52 + ];
53 +
54 + useIdentity(null);
55 + const derived = arr.map(Object.fromEntries);
56 + let t1;
57 + if ($[0] !== derived) {
58 + t1 = derived.at(0);
59 + $[0] = derived;
60 + $[1] = t1;
61 + } else {
62 + t1 = $[1];
63 + }
64 + let t2;
65 + if ($[2] !== derived) {
66 + t2 = derived.at(-1);
67 + $[2] = derived;
68 + $[3] = t2;
69 + } else {
70 + t2 = $[3];
71 + }
72 + let t3;
73 + if ($[4] !== t1 || $[5] !== t2) {
74 + t3 = (
75 + <Stringify>
76 + {t1}
77 + {t2}
78 + </Stringify>
79 + );
80 + $[4] = t1;
81 + $[5] = t2;
82 + $[6] = t3;
83 + } else {
84 + t3 = $[6];
85 + }
86 + return t3;
87 +}
88 +
89 +export const FIXTURE_ENTRYPOINT = {
90 + fn: Component,
91 + params: [{ value: 5 }],
92 + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
93 +};
94 +
95 +```
96 +
97 +### Eval output
98 +(kind: ok) <div>{"children":[{"foo":2},{"baz":5}]}</div>
99 +<div>{"children":[{"foo":2},{"baz":6}]}</div>
100 +<div>{"children":[{"foo":2},{"baz":6}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.tsx new
+27
@@ -0,0 +1,27 @@
1 +import {Stringify, useIdentity} from 'shared-runtime';
2 +
3 +/**
4 + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global
5 + * function that does *not* mutate its operands.
6 + */
7 +function Component({value}) {
8 + const arr = [
9 + new Set([['foo', 2]]).values(),
10 + new Set([['bar', 4]]).values(),
11 + [['baz', value]],
12 + ];
13 + useIdentity(null);
14 + const derived = arr.map(Object.fromEntries);
15 + return (
16 + <Stringify>
17 + {derived.at(0)}
18 + {derived.at(-1)}
19 + </Stringify>
20 + );
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{value: 5}],
26 + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
27 +};