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 670cbd01fcfa..68f8e199dbb5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -10,8 +10,10 @@ import { BUILTIN_SHAPES, BuiltInArrayId, BuiltInFireId, + BuiltInMapId, BuiltInMixedReadonlyId, BuiltInObjectId, + BuiltInSetId, BuiltInUseActionStateId, BuiltInUseContextHookId, BuiltInUseEffectHookId, @@ -458,6 +460,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Primitive, }), ], + [ + 'Map', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutate], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInMapId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], + [ + 'Set', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutate], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInSetId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], // TODO: rest of Global objects ]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 5de4d9ba0c9e..d2e989890694 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -10,7 +10,7 @@ import * as t from '@babel/types'; import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError'; import {assertExhaustive} from '../Utils/utils'; import {Environment, ReactFunctionType} from './Environment'; -import {HookKind} from './ObjectShape'; +import type {HookKind} from './ObjectShape'; import {Type, makeType} from './Types'; import {z} from 'zod'; @@ -829,6 +829,13 @@ export type CallExpression = { typeArguments?: Array; }; +export type NewExpression = { + kind: 'NewExpression'; + callee: Place; + args: Array; + loc: SourceLocation; +}; + export type LoadLocal = { kind: 'LoadLocal'; place: Place; @@ -894,12 +901,7 @@ export type InstructionValue = right: Place; loc: SourceLocation; } - | { - kind: 'NewExpression'; - callee: Place; - args: Array; - loc: SourceLocation; - } + | NewExpression | CallExpression | MethodCall | { @@ -1649,6 +1651,14 @@ export function isArrayType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray'; } +export function isMapType(id: Identifier): boolean { + return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInMap'; +} + +export function isSetType(id: Identifier): boolean { + return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInSet'; +} + export function isPropsType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInProps'; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 22ae261867b3..75aa38a71a45 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -44,6 +44,7 @@ export function addFunction( properties: Iterable<[string, BuiltInType | PolyType]>, fn: Omit, id: string | null = null, + isConstructor: boolean = false, ): FunctionType { const shapeId = id ?? createAnonId(); addShape(registry, shapeId, properties, { @@ -54,6 +55,7 @@ export function addFunction( kind: 'Function', return: fn.returnType, shapeId, + isConstructor, }; } @@ -73,6 +75,7 @@ export function addHook( kind: 'Function', return: fn.returnType, shapeId, + isConstructor: false, }; } @@ -198,6 +201,8 @@ export type ObjectShape = { export type ShapeRegistry = Map; export const BuiltInPropsId = 'BuiltInProps'; export const BuiltInArrayId = 'BuiltInArray'; +export const BuiltInSetId = 'BuiltInSet'; +export const BuiltInMapId = 'BuiltInMap'; export const BuiltInFunctionId = 'BuiltInFunction'; export const BuiltInJsxId = 'BuiltInJsx'; export const BuiltInObjectId = 'BuiltInObject'; @@ -451,6 +456,313 @@ addObject(BUILTIN_SHAPES, BuiltInObjectId, [ */ ]); +/* Built-in Set shape */ +addObject(BUILTIN_SHAPES, BuiltInSetId, [ + [ + /** + * add(value) + * Parameters + * value: the value of the element to add to the Set object. + * Returns the Set object with added value. + */ + 'add', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInSetId}, + calleeEffect: Effect.Store, + // returnValueKind is technically dependent on the ValueKind of the set itself + returnValueKind: ValueKind.Mutable, + }), + ], + [ + /** + * clear() + * Parameters none + * Returns undefined + */ + 'clear', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + /** + * setInstance.delete(value) + * Returns true if value was already in Set; otherwise false. + */ + 'delete', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'has', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + ['size', PRIMITIVE_TYPE], + [ + /** + * difference(other) + * Parameters + * other: A Set object, or set-like object. + * Returns a new Set object containing elements in this set but not in the other set. + */ + 'difference', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInSetId}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + /** + * union(other) + * Parameters + * other: A Set object, or set-like object. + * Returns a new Set object containing elements in either this set or the other set. + */ + 'union', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInSetId}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + /** + * symmetricalDifference(other) + * Parameters + * other: A Set object, or set-like object. + * A new Set object containing elements which are in either this set or the other set, but not in both. + */ + 'symmetricalDifference', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInSetId}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + /** + * isSubsetOf(other) + * Parameters + * other: A Set object, or set-like object. + * Returns true if all elements in this set are also in the other set, and false otherwise. + */ + 'isSubsetOf', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + /** + * isSupersetOf(other) + * Parameters + * other: A Set object, or set-like object. + * Returns true if all elements in the other set are also in this set, and false otherwise. + */ + 'isSupersetOf', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + /** + * forEach(callbackFn) + * forEach(callbackFn, thisArg) + */ + 'forEach', + addFunction(BUILTIN_SHAPES, [], { + /** + * see Array.map explanation for why arguments are marked `ConditionallyMutate` + */ + positionalParams: [], + restParam: Effect.ConditionallyMutate, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.ConditionallyMutate, + returnValueKind: ValueKind.Primitive, + noAlias: true, + mutableOnlyIfOperandsAreMutable: true, + }), + ], + /** + * Iterators + */ + [ + 'entries', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'keys', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'values', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], +]); +addObject(BUILTIN_SHAPES, BuiltInMapId, [ + [ + /** + * clear() + * Parameters none + * Returns undefined + */ + 'clear', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'delete', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + 'get', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'has', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Primitive, + }), + ], + [ + /** + * Params + * key: the key of the element to add to the Map object. The key may be + * any JavaScript type (any primitive value or any type of JavaScript + * object). + * value: the value of the element to add to the Map object. + * Returns the Map object. + */ + 'set', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [Effect.Capture, Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInMapId}, + calleeEffect: Effect.Store, + returnValueKind: ValueKind.Mutable, + }), + ], + ['size', PRIMITIVE_TYPE], + [ + 'forEach', + addFunction(BUILTIN_SHAPES, [], { + /** + * see Array.map explanation for why arguments are marked `ConditionallyMutate` + */ + positionalParams: [], + restParam: Effect.ConditionallyMutate, + returnType: PRIMITIVE_TYPE, + calleeEffect: Effect.ConditionallyMutate, + returnValueKind: ValueKind.Primitive, + noAlias: true, + mutableOnlyIfOperandsAreMutable: true, + }), + ], + /** + * Iterators + */ + [ + 'entries', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'keys', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], + [ + 'values', + addFunction(BUILTIN_SHAPES, [], { + positionalParams: [], + restParam: null, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.Capture, + returnValueKind: ValueKind.Mutable, + }), + ], +]); + addObject(BUILTIN_SHAPES, BuiltInUseStateId, [ ['0', {kind: 'Poly'}], [ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts index 1de81919c391..53eb8a779d73 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts @@ -38,6 +38,7 @@ export type FunctionType = { kind: 'Function'; shapeId: string | null; return: Type; + isConstructor: boolean; }; export type ObjectType = { @@ -111,6 +112,7 @@ export function duplicateType(type: Type): Type { kind: 'Function', return: duplicateType(type.return), shapeId: type.shapeId, + isConstructor: type.isConstructor, }; } case 'Object': { 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 0822a326575f..3b6725db758e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -12,6 +12,7 @@ import { BasicBlock, BlockId, CallExpression, + NewExpression, Effect, FunctionEffect, GeneratedSource, @@ -39,7 +40,6 @@ import { printSourceLocation, } from '../HIR/PrintHIR'; import { - eachCallArgument, eachInstructionOperand, eachInstructionValueOperand, eachPatternOperand, @@ -905,43 +905,12 @@ function inferBlock( break; } case 'NewExpression': { - /** - * For new expressions, we infer a `read` effect on the Class / Function type - * to avoid extending mutable ranges of locally created classes, e.g. - * ```js - * const MyClass = getClass(); - * const value = new MyClass(val1, val2) - * ^ (read) ^ (conditionally mutate) - * ``` - * - * Risks: - * Classes / functions created during render could technically capture and - * mutate their enclosing scope, which we currently do not detect. - */ - const valueKind: AbstractValue = { - kind: ValueKind.Mutable, - reason: new Set([ValueReason.Other]), - context: new Set(), - }; - state.referenceAndRecordEffects( + inferCallEffects( + state, + instr as TInstruction, freezeActions, - instrValue.callee, - Effect.Read, - ValueReason.Other, + getFunctionCallSignature(env, instrValue.callee.identifier.type), ); - - for (const operand of eachCallArgument(instrValue.args)) { - state.referenceAndRecordEffects( - freezeActions, - operand, - Effect.ConditionallyMutate, - ValueReason.Other, - ); - } - - state.initialize(instrValue, valueKind); - state.define(instr.lvalue, instrValue); - instr.lvalue.effect = Effect.ConditionallyMutate; continuation = {kind: 'funeffects'}; break; } @@ -1844,7 +1813,7 @@ export function getFunctionCallSignature( * @returns Inferred effects of function arguments, or null if inference fails. */ export function getFunctionEffects( - fn: MethodCall | CallExpression, + fn: MethodCall | CallExpression | NewExpression, sig: FunctionSignature, ): Array | null { const results = []; @@ -1989,7 +1958,10 @@ function getArgumentEffect( function inferCallEffects( state: InferenceState, - instr: TInstruction | TInstruction, + instr: + | TInstruction + | TInstruction + | TInstruction, freezeActions: Array, signature: FunctionSignature | null, ): void { @@ -2062,9 +2034,7 @@ function inferCallEffects( hasCaptureArgument ||= place.effect === Effect.Capture; } const callee = - instrValue.kind === 'CallExpression' - ? instrValue.callee - : instrValue.receiver; + instrValue.kind === 'MethodCall' ? instrValue.receiver : instrValue.callee; if (signature !== null) { state.referenceAndRecordEffects( freezeActions, @@ -2073,10 +2043,26 @@ function inferCallEffects( ValueReason.Other, ); } else { + /** + * For new expressions, we infer a `read` effect on the Class / Function type + * to avoid extending mutable ranges of locally created classes, e.g. + * ```js + * const MyClass = getClass(); + * const value = new MyClass(val1, val2) + * ^ (read) ^ (conditionally mutate) + * ``` + * + * Risks: + * Classes / functions created during render could technically capture and + * mutate their enclosing scope, which we currently do not detect. + */ + state.referenceAndRecordEffects( freezeActions, callee, - Effect.ConditionallyMutate, + instrValue.kind === 'NewExpression' + ? Effect.Read + : Effect.ConditionallyMutate, ValueReason.Other, ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 02e4e60e4b84..69812fc130de 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -261,6 +261,7 @@ function* generateInstructionTypes( kind: 'Function', shapeId: null, return: returnType, + isConstructor: false, }); yield equation(left, returnType); break; @@ -277,6 +278,7 @@ function* generateInstructionTypes( kind: 'Function', shapeId: null, return: returnType, + isConstructor: false, }); yield equation(left, returnType); break; @@ -333,6 +335,7 @@ function* generateInstructionTypes( kind: 'Function', return: returnType, shapeId: null, + isConstructor: false, }); yield equation(left, returnType); @@ -405,6 +408,7 @@ function* generateInstructionTypes( kind: 'Function', shapeId: BuiltInFunctionId, return: value.loweredFunc.func.returnType, + isConstructor: false, }); break; } @@ -425,9 +429,20 @@ function* generateInstructionTypes( yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId}); break; } + case 'NewExpression': { + const returnType = makeType(); + yield equation(value.callee.identifier.type, { + kind: 'Function', + return: returnType, + shapeId: null, + isConstructor: true, + }); + + yield equation(left, returnType); + break; + } case 'PropertyStore': case 'DeclareLocal': - case 'NewExpression': case 'RegExpLiteral': case 'MetaProperty': case 'ComputedStore': @@ -505,7 +520,11 @@ class Unifier { return; } - if (tB.kind === 'Function' && tA.kind === 'Function') { + if ( + tB.kind === 'Function' && + tA.kind === 'Function' && + tA.isConstructor === tB.isConstructor + ) { this.unify(tA.return, tB.return); return; } @@ -648,6 +667,7 @@ class Unifier { kind: 'Function', return: returnType, shapeId: type.shapeId, + isConstructor: type.isConstructor, }; } case 'ObjectMethod': diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.expect.md new file mode 100644 index 000000000000..61fe33680fa7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Map(); + s.set(el1, makeArray(el1)); + s.set(el2, makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeArray } from "shared-runtime"; + +function useHook(t0) { + const $ = _c(7); + const { el1, el2 } = t0; + let s; + if ($[0] !== el1 || $[1] !== el2) { + s = new Map(); + let t1; + if ($[3] !== el1) { + t1 = makeArray(el1); + $[3] = el1; + $[4] = t1; + } else { + t1 = $[4]; + } + s.set(el1, t1); + let t2; + if ($[5] !== el2) { + t2 = makeArray(el2); + $[5] = el2; + $[6] = t2; + } else { + t2 = $[6]; + } + s.set(el2, t2); + $[0] = el1; + $[1] = el2; + $[2] = s; + } else { + s = $[2]; + } + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ el1: 1, el2: "foo" }], + sequentialRenders: [ + { el1: 1, el2: "foo" }, + { el1: 2, el2: "foo" }, + ], +}; + +``` + +### Eval output +(kind: ok) 2 +2 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.ts new file mode 100644 index 000000000000..2a0fb6d23907 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/map-constructor.ts @@ -0,0 +1,17 @@ +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Map(); + s.set(el1, makeArray(el1)); + s.set(el2, makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.expect.md new file mode 100644 index 000000000000..cb829ffea217 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.expect.md @@ -0,0 +1,76 @@ + +## Input + +```javascript +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeArray } from "shared-runtime"; + +function useHook(t0) { + const $ = _c(5); + const { el1, el2 } = t0; + let s; + if ($[0] !== el1 || $[1] !== el2) { + s = new Set(); + const arr = makeArray(el1); + s.add(arr); + + arr.push(el2); + let t1; + if ($[3] !== el2) { + t1 = makeArray(el2); + $[3] = el2; + $[4] = t1; + } else { + t1 = $[4]; + } + s.add(t1); + $[0] = el1; + $[1] = el2; + $[2] = s; + } else { + s = $[2]; + } + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ el1: 1, el2: "foo" }], + sequentialRenders: [ + { el1: 1, el2: "foo" }, + { el1: 2, el2: "foo" }, + ], +}; + +``` + +### Eval output +(kind: ok) 2 +2 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.ts new file mode 100644 index 000000000000..fe49ba813b0c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-add-mutate.ts @@ -0,0 +1,21 @@ +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.expect.md new file mode 100644 index 000000000000..3d640c5d2abc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.expect.md @@ -0,0 +1,98 @@ + +## Input + +```javascript +const MODULE_LOCAL = new Set([4, 5, 6]); +function useFoo({propArr}: {propArr: Array}) { + /* TODO: Array can be memoized separately of the Set */ + const s1 = new Set([1, 2, 3]); + s1.add(propArr[0]); + + /* but `.values` cannot be memoized separately */ + const s2 = new Set(MODULE_LOCAL.values()); + s2.add(propArr[1]); + + const s3 = new Set(s2.values()); + s3.add(propArr[2]); + + /** + * TODO: s3 should be memoized separately of s4 + */ + const s4 = new Set(s3); + s4.add(propArr[3]); + return [s1, s2, s3, s4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [{propArr: [7, 8, 9]}, {propArr: [7, 8, 10]}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +const MODULE_LOCAL = new Set([4, 5, 6]); +function useFoo(t0) { + const $ = _c(13); + const { propArr } = t0; + let s1; + if ($[0] !== propArr[0]) { + s1 = new Set([1, 2, 3]); + s1.add(propArr[0]); + $[0] = propArr[0]; + $[1] = s1; + } else { + s1 = $[1]; + } + let s2; + let s3; + let s4; + if ($[2] !== propArr[1] || $[3] !== propArr[2] || $[4] !== propArr[3]) { + s2 = new Set(MODULE_LOCAL.values()); + s2.add(propArr[1]); + + s3 = new Set(s2.values()); + s3.add(propArr[2]); + + s4 = new Set(s3); + s4.add(propArr[3]); + $[2] = propArr[1]; + $[3] = propArr[2]; + $[4] = propArr[3]; + $[5] = s2; + $[6] = s3; + $[7] = s4; + } else { + s2 = $[5]; + s3 = $[6]; + s4 = $[7]; + } + let t1; + if ($[8] !== s1 || $[9] !== s2 || $[10] !== s3 || $[11] !== s4) { + t1 = [s1, s2, s3, s4]; + $[8] = s1; + $[9] = s2; + $[10] = s3; + $[11] = s4; + $[12] = t1; + } else { + t1 = $[12]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ propArr: [7, 8, 9] }], + sequentialRenders: [{ propArr: [7, 8, 9] }, { propArr: [7, 8, 10] }], +}; + +``` + +### Eval output +(kind: ok) [{"kind":"Set","value":[1,2,3,7]},{"kind":"Set","value":[4,5,6,8]},{"kind":"Set","value":[4,5,6,8,9]},{"kind":"Set","value":[4,5,6,8,9,null]}] +[{"kind":"Set","value":[1,2,3,7]},{"kind":"Set","value":[4,5,6,8]},{"kind":"Set","value":[4,5,6,8,10]},{"kind":"Set","value":[4,5,6,8,10,null]}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.ts new file mode 100644 index 000000000000..9c98fc6e3270 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.ts @@ -0,0 +1,26 @@ +const MODULE_LOCAL = new Set([4, 5, 6]); +function useFoo({propArr}: {propArr: Array}) { + /* TODO: Array can be memoized separately of the Set */ + const s1 = new Set([1, 2, 3]); + s1.add(propArr[0]); + + /* but `.values` cannot be memoized separately */ + const s2 = new Set(MODULE_LOCAL.values()); + s2.add(propArr[1]); + + const s3 = new Set(s2.values()); + s3.add(propArr[2]); + + /** + * TODO: s3 should be memoized separately of s4 + */ + const s4 = new Set(s3); + s4.add(propArr[3]); + return [s1, s2, s3, s4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [{propArr: [7, 8, 9]}, {propArr: [7, 8, 10]}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.expect.md new file mode 100644 index 000000000000..371e98089f5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Set(); + s.add(makeArray(el1)); + s.add(makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeArray } from "shared-runtime"; + +function useHook(t0) { + const $ = _c(7); + const { el1, el2 } = t0; + let s; + if ($[0] !== el1 || $[1] !== el2) { + s = new Set(); + let t1; + if ($[3] !== el1) { + t1 = makeArray(el1); + $[3] = el1; + $[4] = t1; + } else { + t1 = $[4]; + } + s.add(t1); + let t2; + if ($[5] !== el2) { + t2 = makeArray(el2); + $[5] = el2; + $[6] = t2; + } else { + t2 = $[6]; + } + s.add(t2); + $[0] = el1; + $[1] = el2; + $[2] = s; + } else { + s = $[2]; + } + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ el1: 1, el2: "foo" }], + sequentialRenders: [ + { el1: 1, el2: "foo" }, + { el1: 2, el2: "foo" }, + ], +}; + +``` + +### Eval output +(kind: ok) 2 +2 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.ts new file mode 100644 index 000000000000..049e411d53ee --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor.ts @@ -0,0 +1,17 @@ +import {makeArray} from 'shared-runtime'; + +function useHook({el1, el2}) { + const s = new Set(); + s.add(makeArray(el1)); + s.add(makeArray(el2)); + return s.size; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{el1: 1, el2: 'foo'}], + sequentialRenders: [ + {el1: 1, el2: 'foo'}, + {el1: 2, el2: 'foo'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.expect.md new file mode 100644 index 000000000000..d5fcb7f73de0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.expect.md @@ -0,0 +1,82 @@ + +## Input + +```javascript +import {makeArray, mutate} from 'shared-runtime'; + +function useFoo({propArr}: {propArr: Array}) { + const s1 = new Set>([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + const s2 = new Set(s1); + // this may also may mutate s1 + mutate(s2); + + return [s1, s2]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [ + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 10]}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeArray, mutate } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(6); + const { propArr } = t0; + let s1; + let s2; + if ($[0] !== propArr[0]) { + s1 = new Set([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + s2 = new Set(s1); + + mutate(s2); + $[0] = propArr[0]; + $[1] = s1; + $[2] = s2; + } else { + s1 = $[1]; + s2 = $[2]; + } + let t1; + if ($[3] !== s1 || $[4] !== s2) { + t1 = [s1, s2]; + $[3] = s1; + $[4] = s2; + $[5] = t1; + } else { + t1 = $[5]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ propArr: [7, 8, 9] }], + sequentialRenders: [ + { propArr: [7, 8, 9] }, + { propArr: [7, 8, 9] }, + { propArr: [7, 8, 10] }, + ], +}; + +``` + +### Eval output +(kind: ok) [{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] +[{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] +[{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.ts new file mode 100644 index 000000000000..7bd283371e00 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-copy-constructor-mutate.ts @@ -0,0 +1,22 @@ +import {makeArray, mutate} from 'shared-runtime'; + +function useFoo({propArr}: {propArr: Array}) { + const s1 = new Set>([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + const s2 = new Set(s1); + // this may also may mutate s1 + mutate(s2); + + return [s1, s2]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [ + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 10]}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.expect.md new file mode 100644 index 000000000000..47ac8557dfba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +import {makeArray, useHook} from 'shared-runtime'; + +function useFoo({propArr}: {propArr: Array}) { + const s1 = new Set>([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + useHook(); + const s2 = new Set(); + for (const el of s1.values()) { + s2.add(el); + } + + return [s1, s2]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [ + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 10]}, + ], +}; + +``` + +## Code + +```javascript +import { makeArray, useHook } from "shared-runtime"; + +function useFoo(t0) { + const { propArr } = t0; + const s1 = new Set([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + useHook(); + const s2 = new Set(); + for (const el of s1.values()) { + s2.add(el); + } + return [s1, s2]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ propArr: [7, 8, 9] }], + sequentialRenders: [ + { propArr: [7, 8, 9] }, + { propArr: [7, 8, 9] }, + { propArr: [7, 8, 10] }, + ], +}; + +``` + +### Eval output +(kind: ok) [{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] +[{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] +[{"kind":"Set","value":[1,2,3,[7]]},{"kind":"Set","value":[1,2,3,"[[ cyclic ref *2 ]]"]}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.ts new file mode 100644 index 000000000000..63574c4bc3af --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-for-of-iterate-values.ts @@ -0,0 +1,24 @@ +import {makeArray, useHook} from 'shared-runtime'; + +function useFoo({propArr}: {propArr: Array}) { + const s1 = new Set>([1, 2, 3]); + s1.add(makeArray(propArr[0])); + + useHook(); + const s2 = new Set(); + for (const el of s1.values()) { + s2.add(el); + } + + return [s1, s2]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{propArr: [7, 8, 9]}], + sequentialRenders: [ + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 9]}, + {propArr: [7, 8, 10]}, + ], +};