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 df8196c1d7a0..670cbd01fcfa 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 ae71da64b419..0822a326575f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -23,6 +23,7 @@ import { Phi, Place, SpreadPattern, + TInstruction, Type, ValueKind, ValueReason, @@ -251,7 +252,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 +268,7 @@ class InferenceState { values: Map, variables: Map>, ) { - this.#env = env; + this.env = env; this.#values = values; this.#variables = variables; } @@ -409,8 +410,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 +591,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 +605,7 @@ class InferenceState { */ clone(): InferenceState { return new InferenceState( - this.#env, + this.env, new Map(this.#values), new Map(this.#variables), ); @@ -1238,62 +1239,12 @@ function inferBlock( break; } case 'CallExpression': { - const signature = getFunctionCallSignature( - env, - instrValue.callee.identifier.type, + inferCallEffects( + state, + instr as TInstruction, + freezeActions, + getFunctionCallSignature(env, instrValue.callee.identifier.type), ); - - const effects = - signature !== null ? getFunctionEffects(instrValue, signature) : null; - const returnValueKind: AbstractValue = - signature !== null - ? { - kind: signature.returnValueKind, - reason: new Set([ - signature.returnValueReason ?? - ValueReason.KnownReturnSignature, - ]), - context: new Set(), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - let hasCaptureArgument = false; - for (let i = 0; i < instrValue.args.length; i++) { - const arg = instrValue.args[i]; - const place = arg.kind === 'Identifier' ? arg : arg.place; - state.referenceAndRecordEffects( - freezeActions, - place, - getArgumentEffect(effects != null ? effects[i] : null, arg), - ValueReason.Other, - ); - hasCaptureArgument ||= place.effect === Effect.Capture; - } - if (signature !== null) { - state.referenceAndRecordEffects( - freezeActions, - instrValue.callee, - signature.calleeEffect, - ValueReason.Other, - ); - } else { - state.referenceAndRecordEffects( - freezeActions, - instrValue.callee, - Effect.ConditionallyMutate, - ValueReason.Other, - ); - } - hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture; - - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = hasCaptureArgument - ? Effect.Store - : Effect.ConditionallyMutate; continuation = {kind: 'funeffects'}; break; } @@ -1311,102 +1262,12 @@ function inferBlock( Effect.Read, ValueReason.Other, ); - - const signature = getFunctionCallSignature( - env, - instrValue.property.identifier.type, + inferCallEffects( + state, + instr as TInstruction, + freezeActions, + getFunctionCallSignature(env, instrValue.property.identifier.type), ); - - const returnValueKind: AbstractValue = - signature !== null - ? { - kind: signature.returnValueKind, - reason: new Set([ - signature.returnValueReason ?? - ValueReason.KnownReturnSignature, - ]), - context: new Set(), - } - : { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - - if ( - signature !== null && - signature.mutableOnlyIfOperandsAreMutable && - areArgumentsImmutableAndNonMutating(state, instrValue.args) - ) { - /* - * None of the args are mutable or mutate their params, we can downgrade to - * treating as all reads (except that the receiver may be captured) - */ - for (const arg of instrValue.args) { - const place = arg.kind === 'Identifier' ? arg : arg.place; - state.referenceAndRecordEffects( - freezeActions, - place, - Effect.Read, - ValueReason.Other, - ); - } - state.referenceAndRecordEffects( - freezeActions, - instrValue.receiver, - Effect.Capture, - ValueReason.Other, - ); - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = - instrValue.receiver.effect === Effect.Capture - ? Effect.Store - : Effect.ConditionallyMutate; - continuation = {kind: 'funeffects'}; - break; - } - - const effects = - signature !== null ? getFunctionEffects(instrValue, signature) : null; - let hasCaptureArgument = false; - for (let i = 0; i < instrValue.args.length; i++) { - const arg = instrValue.args[i]; - const place = arg.kind === 'Identifier' ? arg : arg.place; - /* - * If effects are inferred for an argument, we should fail invalid - * mutating effects - */ - state.referenceAndRecordEffects( - freezeActions, - place, - getArgumentEffect(effects != null ? effects[i] : null, arg), - ValueReason.Other, - ); - hasCaptureArgument ||= place.effect === Effect.Capture; - } - if (signature !== null) { - state.referenceAndRecordEffects( - freezeActions, - instrValue.receiver, - signature.calleeEffect, - ValueReason.Other, - ); - } else { - state.referenceAndRecordEffects( - freezeActions, - instrValue.receiver, - Effect.ConditionallyMutate, - ValueReason.Other, - ); - } - hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture; - - state.initialize(instrValue, returnValueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = hasCaptureArgument - ? Effect.Store - : Effect.ConditionallyMutate; continuation = {kind: 'funeffects'}; break; } @@ -2012,6 +1873,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 +1910,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 +1934,10 @@ function areArgumentsImmutableAndNonMutating( break; } default: { + /** + * Globals, module locals, and other locally defined functions may + * mutate their arguments. + */ return false; } } @@ -2085,3 +1986,105 @@ function getArgumentEffect( return Effect.ConditionallyMutate; } } + +function inferCallEffects( + state: InferenceState, + instr: TInstruction | TInstruction, + freezeActions: Array, + signature: FunctionSignature | null, +): void { + const instrValue = instr.value; + const returnValueKind: AbstractValue = + signature !== null + ? { + kind: signature.returnValueKind, + reason: new Set([ + signature.returnValueReason ?? ValueReason.KnownReturnSignature, + ]), + context: new Set(), + } + : { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + context: new Set(), + }; + + if ( + instrValue.kind === 'MethodCall' && + signature !== null && + signature.mutableOnlyIfOperandsAreMutable && + areArgumentsImmutableAndNonMutating(state, instrValue.args) + ) { + /* + * None of the args are mutable or mutate their params, we can downgrade to + * treating as all reads (except that the receiver may be captured) + */ + for (const arg of instrValue.args) { + const place = arg.kind === 'Identifier' ? arg : arg.place; + state.referenceAndRecordEffects( + freezeActions, + place, + Effect.Read, + ValueReason.Other, + ); + } + state.referenceAndRecordEffects( + freezeActions, + instrValue.receiver, + Effect.Capture, + ValueReason.Other, + ); + state.initialize(instrValue, returnValueKind); + state.define(instr.lvalue, instrValue); + instr.lvalue.effect = + instrValue.receiver.effect === Effect.Capture + ? Effect.Store + : Effect.ConditionallyMutate; + return; + } + + const effects = + signature !== null ? getFunctionEffects(instrValue, signature) : null; + let hasCaptureArgument = false; + for (let i = 0; i < instrValue.args.length; i++) { + const arg = instrValue.args[i]; + const place = arg.kind === 'Identifier' ? arg : arg.place; + /* + * If effects are inferred for an argument, we should fail invalid + * mutating effects + */ + state.referenceAndRecordEffects( + freezeActions, + place, + getArgumentEffect(effects != null ? effects[i] : null, arg), + ValueReason.Other, + ); + hasCaptureArgument ||= place.effect === Effect.Capture; + } + const callee = + instrValue.kind === 'CallExpression' + ? instrValue.callee + : instrValue.receiver; + if (signature !== null) { + state.referenceAndRecordEffects( + freezeActions, + callee, + signature.calleeEffect, + ValueReason.Other, + ); + } else { + state.referenceAndRecordEffects( + freezeActions, + callee, + Effect.ConditionallyMutate, + ValueReason.Other, + ); + } + hasCaptureArgument ||= callee.effect === Effect.Capture; + + state.initialize(instrValue, returnValueKind); + state.define(instr.lvalue, instrValue); + instr.lvalue.effect = hasCaptureArgument + ? Effect.Store + : Effect.ConditionallyMutate; +} 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 586124280a3d..9be174d99864 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 edb4e3712584..4e224c8a9ac8 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 000000000000..091d050e6400 --- /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 000000000000..33e418a5fda7 --- /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 000000000000..0812e46c55be --- /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 000000000000..cd676d9b9075 --- /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 000000000000..b6bd4709ca6b --- /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 000000000000..bda94b92c837 --- /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 000000000000..27735532326f --- /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 000000000000..191d0e0d3360 --- /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}], +};