From cb78d82577473bbec8d7a32d83a81e72661bc1ff Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Thu, 20 Mar 2025 19:46:41 -0400 Subject: [PATCH] [compiler][fix] mutableOnlyIfOperandsAreMutable does not apply when operands are globals Globals, module locals, and other locally defined functions may mutate their arguments. See test fixtures for details --- .../src/HIR/Globals.ts | 110 ++++++++++++++-- .../src/Inference/InferReferenceEffects.ts | 54 ++++++-- .../array-from-maybemutates-arg0.expect.md | 39 ++++-- .../compiler/array-from-maybemutates-arg0.js | 7 +- ...-array-filter-capture-mutate-bug.expect.md | 113 +++++++++++++++++ .../repro-array-filter-capture-mutate-bug.tsx | 34 +++++ ...y-filter-known-nonmutate-Boolean.expect.md | 118 ++++++++++++++++++ ...o-array-filter-known-nonmutate-Boolean.tsx | 23 ++++ ...pro-array-map-capture-mutate-bug.expect.md | 91 ++++++++++++++ .../repro-array-map-capture-mutate-bug.tsx | 23 ++++ ...pro-array-map-known-mutate-shape.expect.md | 100 +++++++++++++++ .../repro-array-map-known-mutate-shape.tsx | 27 ++++ 12 files changed, 709 insertions(+), 30 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index df8196c1d7a0d..670cbd01fcfaf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -11,6 +11,7 @@ import { BuiltInArrayId, BuiltInFireId, BuiltInMixedReadonlyId, + BuiltInObjectId, BuiltInUseActionStateId, BuiltInUseContextHookId, BuiltInUseEffectHookId, @@ -45,21 +46,17 @@ export const DEFAULT_SHAPES: ShapeRegistry = new Map(BUILTIN_SHAPES); // Hack until we add ObjectShapes for all globals const UNTYPED_GLOBALS: Set = new Set([ - 'String', 'Object', 'Function', - 'Number', 'RegExp', 'Date', 'Error', - 'Function', 'TypeError', 'RangeError', 'ReferenceError', 'SyntaxError', 'URIError', 'EvalError', - 'Boolean', 'DataView', 'Float32Array', 'Float64Array', @@ -75,16 +72,8 @@ const UNTYPED_GLOBALS: Set = new Set([ 'Uint32Array', 'ArrayBuffer', 'JSON', - 'parseFloat', - 'parseInt', 'console', - 'isNaN', 'eval', - 'isFinite', - 'encodeURI', - 'decodeURI', - 'encodeURIComponent', - 'decodeURIComponent', ]); const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ @@ -101,6 +90,23 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Mutable, }), ], + [ + /** + * Object.fromEntries(iterable) + * iterable: An iterable, such as an Array or Map, containing a list of + * objects. Each object should have two properties. + * Returns a new object whose properties are given by the entries of the + * iterable. + */ + 'fromEntries', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.ConditionallyMutate], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInObjectId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }), + ], ]), ], [ @@ -372,6 +378,86 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Primitive, }), ], + [ + 'parseInt', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'parseFloat', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'isNaN', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'isFinite', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'encodeURI', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'encodeURIComponent', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'decodeURI', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'decodeURIComponent', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [], + restParam: Effect.Read, + returnType: {kind: 'Primitive'}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], // TODO: rest of Global objects ]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index ae71da64b4191..5874fcfb06c99 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -251,7 +251,7 @@ type FreezeAction = {values: Set; reason: Set}; // Maintains a mapping of top-level variables to the kind of value they hold class InferenceState { - #env: Environment; + env: Environment; // The kind of each value, based on its allocation site #values: Map; @@ -267,7 +267,7 @@ class InferenceState { values: Map, variables: Map>, ) { - this.#env = env; + this.env = env; this.#values = values; this.#variables = variables; } @@ -409,8 +409,8 @@ class InferenceState { }); if ( value.kind === 'FunctionExpression' && - (this.#env.config.enablePreserveExistingMemoizationGuarantees || - this.#env.config.enableTransitivelyFreezeFunctionExpressions) + (this.env.config.enablePreserveExistingMemoizationGuarantees || + this.env.config.enableTransitivelyFreezeFunctionExpressions) ) { for (const operand of value.loweredFunc.func.context) { const operandValues = this.#variables.get(operand.identifier.id); @@ -590,7 +590,7 @@ class InferenceState { return null; } else { return new InferenceState( - this.#env, + this.env, nextValues ?? new Map(this.#values), nextVariables ?? new Map(this.#variables), ); @@ -604,7 +604,7 @@ class InferenceState { */ clone(): InferenceState { return new InferenceState( - this.#env, + this.env, new Map(this.#values), new Map(this.#variables), ); @@ -2012,6 +2012,32 @@ export function getFunctionEffects( return results; } +export function isKnownMutableEffect(effect: Effect): boolean { + switch (effect) { + case Effect.Store: + case Effect.ConditionallyMutate: + case Effect.Mutate: { + return true; + } + + case Effect.Unknown: { + CompilerError.invariant(false, { + reason: 'Unexpected unknown effect', + description: null, + loc: GeneratedSource, + suggestions: null, + }); + } + case Effect.Read: + case Effect.Capture: + case Effect.Freeze: { + return false; + } + default: { + assertExhaustive(effect, `Unexpected effect \`${effect}\``); + } + } +} /** * Returns true if all of the arguments are both non-mutable (immutable or frozen) * _and_ are not functions which might mutate their arguments. Note that function @@ -2023,10 +2049,20 @@ function areArgumentsImmutableAndNonMutating( args: MethodCall['args'], ): boolean { for (const arg of args) { + if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') { + const fnShape = state.env.getFunctionSignature(arg.identifier.type); + if (fnShape != null) { + return ( + !fnShape.positionalParams.some(isKnownMutableEffect) && + (fnShape.restParam == null || + !isKnownMutableEffect(fnShape.restParam)) + ); + } + } const place = arg.kind === 'Identifier' ? arg : arg.place; + const kind = state.kind(place).kind; switch (kind) { - case ValueKind.Global: case ValueKind.Primitive: case ValueKind.Frozen: { /* @@ -2037,6 +2073,10 @@ function areArgumentsImmutableAndNonMutating( break; } default: { + /** + * Globals, module locals, and other locally defined functions may + * mutate their arguments. + */ return false; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.expect.md index 586124280a3dd..9be174d99864c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.expect.md @@ -8,7 +8,12 @@ function Component({value}) { const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; useIdentity(); const derived = Array.from(arr, mutateAndReturn); - return {derived.at(-1)}; + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); } export const FIXTURE_ENTRYPOINT = { @@ -26,28 +31,42 @@ import { c as _c } from "react/compiler-runtime"; import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime"; function Component(t0) { - const $ = _c(4); + const $ = _c(7); const { value } = t0; const arr = [{ value: "foo" }, { value: "bar" }, { value }]; useIdentity(); const derived = Array.from(arr, mutateAndReturn); let t1; if ($[0] !== derived) { - t1 = derived.at(-1); + t1 = derived.at(0); $[0] = derived; $[1] = t1; } else { t1 = $[1]; } let t2; - if ($[2] !== t1) { - t2 = {t1}; - $[2] = t1; + if ($[2] !== derived) { + t2 = derived.at(-1); + $[2] = derived; $[3] = t2; } else { t2 = $[3]; } - return t2; + let t3; + if ($[4] !== t1 || $[5] !== t2) { + t3 = ( + + {t1} + {t2} + + ); + $[4] = t1; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; } export const FIXTURE_ENTRYPOINT = { @@ -59,6 +78,6 @@ export const FIXTURE_ENTRYPOINT = { ``` ### Eval output -(kind: ok)
{"children":{"value":5,"wat0":"joe"}}
-
{"children":{"value":6,"wat0":"joe"}}
-
{"children":{"value":6,"wat0":"joe"}}
\ No newline at end of file +(kind: ok)
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.js index edb4e37125843..4e224c8a9ac8e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-maybemutates-arg0.js @@ -4,7 +4,12 @@ function Component({value}) { const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; useIdentity(); const derived = Array.from(arr, mutateAndReturn); - return {derived.at(-1)}; + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.expect.md new file mode 100644 index 0000000000000..091d050e64008 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.expect.md @@ -0,0 +1,113 @@ + +## Input + +```javascript +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +/** + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ * Forget: + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(mutateAndReturn); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime"; + +/** + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ * Forget: + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ + */ +function Component(t0) { + const $ = _c(7); + const { value } = t0; + const arr = [{ value: "foo" }, { value: "bar" }, { value }]; + useIdentity(null); + const derived = arr.filter(mutateAndReturn); + let t1; + if ($[0] !== derived) { + t1 = derived.at(0); + $[0] = derived; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] !== derived) { + t2 = derived.at(-1); + $[2] = derived; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t1 || $[5] !== t2) { + t3 = ( + + {t1} + {t2} + + ); + $[4] = t1; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 5 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }], +}; + +``` + +### Eval output +(kind: ok)
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.tsx new file mode 100644 index 0000000000000..33e418a5fda7e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-capture-mutate-bug.tsx @@ -0,0 +1,34 @@ +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +/** + * Repro for bug with `mutableOnlyIfOperandsAreMutable` flag + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+ * Forget: + * (kind: ok) + *
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ *
{"children":[{"value":"foo","wat0":"joe","wat1":"joe"},{"value":6,"wat0":"joe"}]}
+ + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(mutateAndReturn); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.expect.md new file mode 100644 index 0000000000000..0812e46c55be2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.expect.md @@ -0,0 +1,118 @@ + +## Input + +```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Also see repro-array-map-known-mutate-shape, which calls a global function + * that mutates its operands. + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(Boolean); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + +/** + * Also see repro-array-map-known-mutate-shape, which calls a global function + * that mutates its operands. + */ +function Component(t0) { + const $ = _c(13); + const { value } = t0; + let t1; + let t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { value: "foo" }; + t2 = { value: "bar" }; + $[0] = t1; + $[1] = t2; + } else { + t1 = $[0]; + t2 = $[1]; + } + let t3; + if ($[2] !== value) { + t3 = [t1, t2, { value }]; + $[2] = value; + $[3] = t3; + } else { + t3 = $[3]; + } + const arr = t3; + useIdentity(null); + let t4; + if ($[4] !== arr) { + t4 = arr.filter(Boolean); + $[4] = arr; + $[5] = t4; + } else { + t4 = $[5]; + } + const derived = t4; + let t5; + if ($[6] !== derived) { + t5 = derived.at(0); + $[6] = derived; + $[7] = t5; + } else { + t5 = $[7]; + } + let t6; + if ($[8] !== derived) { + t6 = derived.at(-1); + $[8] = derived; + $[9] = t6; + } else { + t6 = $[9]; + } + let t7; + if ($[10] !== t5 || $[11] !== t6) { + t7 = ( + + {t5} + {t6} + + ); + $[10] = t5; + $[11] = t6; + $[12] = t7; + } else { + t7 = $[12]; + } + return t7; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 5 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }], +}; + +``` + +### Eval output +(kind: ok)
{"children":[{"value":"foo"},{"value":5}]}
+
{"children":[{"value":"foo"},{"value":6}]}
+
{"children":[{"value":"foo"},{"value":6}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.tsx new file mode 100644 index 0000000000000..cd676d9b9075a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-filter-known-nonmutate-Boolean.tsx @@ -0,0 +1,23 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Also see repro-array-map-known-mutate-shape, which calls a global function + * that mutates its operands. + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(Boolean); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.expect.md new file mode 100644 index 0000000000000..b6bd4709ca6b4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.expect.md @@ -0,0 +1,91 @@ + +## Input + +```javascript +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +/** + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any + * function call which captures its callee when applying an operand. + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.map(mutateAndReturn); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime"; + +/** + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any + * function call which captures its callee when applying an operand. + */ +function Component(t0) { + const $ = _c(7); + const { value } = t0; + const arr = [{ value: "foo" }, { value: "bar" }, { value }]; + useIdentity(null); + const derived = arr.map(mutateAndReturn); + let t1; + if ($[0] !== derived) { + t1 = derived.at(0); + $[0] = derived; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] !== derived) { + t2 = derived.at(-1); + $[2] = derived; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t1 || $[5] !== t2) { + t3 = ( + + {t1} + {t2} + + ); + $[4] = t1; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 5 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }], +}; + +``` + +### Eval output +(kind: ok)
{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.tsx new file mode 100644 index 0000000000000..bda94b92c8372 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-capture-mutate-bug.tsx @@ -0,0 +1,23 @@ +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +/** + * Copy of repro-array-map-capture-mutate-bug, showing that the same issue applies to any + * function call which captures its callee when applying an operand. + */ +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.map(mutateAndReturn); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.expect.md new file mode 100644 index 0000000000000..27735532326ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.expect.md @@ -0,0 +1,100 @@ + +## Input + +```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global + * function that does *not* mutate its operands. + */ +function Component({value}) { + const arr = [ + new Set([['foo', 2]]).values(), + new Set([['bar', 4]]).values(), + [['baz', value]], + ]; + useIdentity(null); + const derived = arr.map(Object.fromEntries); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + +/** + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global + * function that does *not* mutate its operands. + */ +function Component(t0) { + const $ = _c(7); + const { value } = t0; + const arr = [ + new Set([["foo", 2]]).values(), + new Set([["bar", 4]]).values(), + [["baz", value]], + ]; + + useIdentity(null); + const derived = arr.map(Object.fromEntries); + let t1; + if ($[0] !== derived) { + t1 = derived.at(0); + $[0] = derived; + $[1] = t1; + } else { + t1 = $[1]; + } + let t2; + if ($[2] !== derived) { + t2 = derived.at(-1); + $[2] = derived; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t1 || $[5] !== t2) { + t3 = ( + + {t1} + {t2} + + ); + $[4] = t1; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 5 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }], +}; + +``` + +### Eval output +(kind: ok)
{"children":[{"foo":2},{"baz":5}]}
+
{"children":[{"foo":2},{"baz":6}]}
+
{"children":[{"foo":2},{"baz":6}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.tsx new file mode 100644 index 0000000000000..191d0e0d33607 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/repro-array-map-known-mutate-shape.tsx @@ -0,0 +1,27 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + +/** + * Also see repro-array-map-known-nonmutate-Boolean, which calls a global + * function that does *not* mutate its operands. + */ +function Component({value}) { + const arr = [ + new Set([['foo', 2]]).values(), + new Set([['bar', 4]]).values(), + [['baz', value]], + ]; + useIdentity(null); + const derived = arr.map(Object.fromEntries); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], +};