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 670cbd01fc..37a8816d0b 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, @@ -63,8 +65,6 @@ const UNTYPED_GLOBALS: Set = new Set([ 'Int8Array', 'Int16Array', 'Int32Array', - 'Map', - 'Set', 'WeakMap', 'Uint8Array', 'Uint8ClampedArray', @@ -138,7 +138,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ 'from', addFunction(DEFAULT_SHAPES, [], { positionalParams: [ - Effect.ConditionallyMutate, + Effect.ConditionallyMutateIterator, Effect.ConditionallyMutate, Effect.ConditionallyMutate, ], @@ -458,6 +458,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Primitive, }), ], + [ + 'Map', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutateIterator], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInMapId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + }, + null, + true, + ), + ], + [ + 'Set', + addFunction( + DEFAULT_SHAPES, + [], + { + positionalParams: [Effect.ConditionallyMutateIterator], + 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 5de4d9ba0c..f58cdfbb08 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 | { @@ -1394,6 +1396,7 @@ export enum Effect { Read = 'read', // This reference reads and stores the value Capture = 'capture', + ConditionallyMutateIterator = 'mutate-iterator?', /* * This reference *may* write to (mutate) the value. This covers two similar cases: * - The compiler is being conservative and assuming that a value *may* be mutated @@ -1412,11 +1415,11 @@ export enum Effect { // This reference may alias to (mutate) the value Store = 'store', } - export const EffectSchema = z.enum([ Effect.Read, Effect.Mutate, Effect.ConditionallyMutate, + Effect.ConditionallyMutateIterator, Effect.Capture, Effect.Store, Effect.Freeze, @@ -1430,6 +1433,7 @@ export function isMutableEffect( case Effect.Capture: case Effect.Store: case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: case Effect.Mutate: { return true; } @@ -1649,6 +1653,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 22ae261867..143d23d6d5 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,311 @@ 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: [], + 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, + }), + ], + /** + * Iteraturs + */ + [ + '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, + }), + ], + /** + * Iteraturs + */ + [ + '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 1de81919c3..53eb8a779d 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/InferMutableLifetimes.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts index 508a970d93..45b5462efb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts @@ -11,7 +11,10 @@ import { Identifier, InstructionId, InstructionKind, + isArrayType, + isMapType, isRefOrRefValue, + isSetType, makeInstructionId, Place, } from '../HIR/HIR'; @@ -90,6 +93,17 @@ function inferPlace( infer(place, instrId); } return; + case Effect.ConditionallyMutateIterator: { + const identifier = place.identifier; + if ( + !isArrayType(identifier) && + !isSetType(identifier) && + !isMapType(identifier) + ) { + infer(place, instrId); + } + return; + } case Effect.ConditionallyMutate: case Effect.Mutate: { infer(place, instrId); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts index 344949b020..e2deab15db 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts @@ -230,6 +230,7 @@ export function inferReactivePlaces(fn: HIRFunction): void { case Effect.Capture: case Effect.Store: case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: case Effect.Mutate: { if (isMutable(instruction, operand)) { reactiveIdentifiers.markReactive(operand); 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 5874fcfb06..7db4327b04 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, @@ -23,12 +24,15 @@ import { Phi, Place, SpreadPattern, + TInstruction, Type, ValueKind, ValueReason, isArrayType, + isMapType, isMutableEffect, isObjectType, + isSetType, } from '../HIR/HIR'; import {FunctionSignature} from '../HIR/ObjectShape'; import { @@ -468,6 +472,25 @@ class InferenceState { } break; } + case Effect.ConditionallyMutateIterator: { + if ( + valueKind.kind === ValueKind.Mutable || + valueKind.kind === ValueKind.Context + ) { + if ( + isArrayType(place.identifier) || + isSetType(place.identifier) || + isMapType(place.identifier) + ) { + effect = Effect.Capture; + } else { + effect = Effect.ConditionallyMutate; + } + } else { + effect = Effect.Read; + } + break; + } case Effect.Mutate: { effect = Effect.Mutate; break; @@ -879,9 +902,7 @@ function inferBlock( state.referenceAndRecordEffects( freezeActions, element.place, - isArrayType(element.place.identifier) - ? Effect.Capture - : Effect.ConditionallyMutate, + Effect.ConditionallyMutateIterator, ValueReason.Other, ); } else if (element.kind === 'Identifier') { @@ -904,43 +925,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; } @@ -1238,62 +1228,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 +1251,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; } @@ -1813,7 +1663,13 @@ function inferBlock( kind === ValueKind.Mutable || kind === ValueKind.Context; let effect; let valueKind: AbstractValue; - if (!isMutable || isArrayType(instrValue.collection.identifier)) { + const iterator = instrValue.collection.identifier; + if ( + !isMutable || + isArrayType(iterator) || + isMapType(iterator) || + isSetType(iterator) + ) { // Case 1, assume iterator is a separate mutable object effect = { kind: Effect.Read, @@ -1854,7 +1710,7 @@ function inferBlock( state.referenceAndRecordEffects( freezeActions, instrValue.iterator, - Effect.ConditionallyMutate, + Effect.ConditionallyMutateIterator, ValueReason.Other, ); /** @@ -1983,7 +1839,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 = []; @@ -2016,6 +1872,7 @@ export function isKnownMutableEffect(effect: Effect): boolean { switch (effect) { case Effect.Store: case Effect.ConditionallyMutate: + case Effect.ConditionallyMutateIterator: case Effect.Mutate: { return true; } @@ -2119,9 +1976,128 @@ function getArgumentEffect( }); } // effects[i] is Effect.Capture | Effect.Read | Effect.Store - return Effect.ConditionallyMutate; + return Effect.ConditionallyMutateIterator; } } else { return Effect.ConditionallyMutate; } } + +function inferCallEffects( + state: InferenceState, + instr: + | TInstruction + | 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 === 'MethodCall' ? instrValue.receiver : instrValue.callee; + if (signature !== null) { + state.referenceAndRecordEffects( + freezeActions, + callee, + signature.calleeEffect, + 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, + instrValue.kind === 'NewExpression' + ? Effect.Read + : 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/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 02e4e60e4b..a48d469ab6 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': @@ -506,8 +521,10 @@ class Unifier { } if (tB.kind === 'Function' && tA.kind === 'Function') { - this.unify(tA.return, tB.return); - return; + if (tA.isConstructor === tB.isConstructor) { + this.unify(tA.return, tB.return); + return; + } } } @@ -648,6 +665,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/array-from-arg1-captures-arg0.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-arg1-captures-arg0.expect.md index 8892c8d484..12be224d8f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-arg1-captures-arg0.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-arg1-captures-arg0.expect.md @@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime"; * (2) the 1st argument might mutate its callee */ function Component(t0) { - const $ = _c(4); + const $ = _c(10); const { value } = t0; - const arr = [{ value: "foo" }, { value: "bar" }, { value }]; - useIdentity(); - const derived = Array.from(arr, _temp); let t1; - if ($[0] !== derived) { - t1 = derived.at(-1); - $[0] = derived; - $[1] = t1; - } else { - t1 = $[1]; - } let t2; - if ($[2] !== t1) { - t2 = {t1}; - $[2] = t1; - $[3] = t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { value: "foo" }; + t2 = { value: "bar" }; + $[0] = t1; + $[1] = t2; } else { - t2 = $[3]; + t1 = $[0]; + t2 = $[1]; } - return t2; + let t3; + if ($[2] !== value) { + t3 = [t1, t2, { value }]; + $[2] = value; + $[3] = t3; + } else { + t3 = $[3]; + } + const arr = t3; + useIdentity(); + let t4; + if ($[4] !== arr) { + t4 = Array.from(arr, _temp); + $[4] = arr; + $[5] = t4; + } else { + t4 = $[5]; + } + const derived = t4; + let t5; + if ($[6] !== derived) { + t5 = derived.at(-1); + $[6] = derived; + $[7] = t5; + } else { + t5 = $[7]; + } + let t6; + if ($[8] !== t5) { + t6 = {t5}; + $[8] = t5; + $[9] = t6; + } else { + t6 = $[9]; + } + return t6; } function _temp(x, idx) { return { ...x, id: idx }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-captures-arg0.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-captures-arg0.expect.md index 66d0b42584..3e89dbeae8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-captures-arg0.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/array-from-captures-arg0.expect.md @@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime"; * (2) the 1st argument might mutate its callee */ function Component(t0) { - const $ = _c(4); + const $ = _c(10); const { value } = t0; - const arr = [{ value: "foo" }, { value: "bar" }, { value }]; - useIdentity(); - const derived = Array.from(arr); let t1; - if ($[0] !== derived) { - t1 = derived.at(-1); - $[0] = derived; - $[1] = t1; - } else { - t1 = $[1]; - } let t2; - if ($[2] !== t1) { - t2 = {t1}; - $[2] = t1; - $[3] = t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { value: "foo" }; + t2 = { value: "bar" }; + $[0] = t1; + $[1] = t2; } else { - t2 = $[3]; + t1 = $[0]; + t2 = $[1]; } - return t2; + let t3; + if ($[2] !== value) { + t3 = [t1, t2, { value }]; + $[2] = value; + $[3] = t3; + } else { + t3 = $[3]; + } + const arr = t3; + useIdentity(); + let t4; + if ($[4] !== arr) { + t4 = Array.from(arr); + $[4] = arr; + $[5] = t4; + } else { + t4 = $[5]; + } + const derived = t4; + let t5; + if ($[6] !== derived) { + t5 = derived.at(-1); + $[6] = derived; + $[7] = t5; + } else { + t5 = $[7]; + } + let t6; + if ($[8] !== t5) { + t6 = {t5}; + $[8] = t5; + $[9] = t6; + } else { + t6 = $[9]; + } + return t6; } export const FIXTURE_ENTRYPOINT = { 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 9be174d998..9421194495 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 @@ -7,7 +7,7 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; function Component({value}) { const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; useIdentity(); - const derived = Array.from(arr, mutateAndReturn); + const derived = Array.from(arr).map(mutateAndReturn); return ( {derived.at(0)} @@ -19,7 +19,7 @@ function Component({value}) { export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{value: 5}], - sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}], }; ``` @@ -35,7 +35,7 @@ function Component(t0) { const { value } = t0; const arr = [{ value: "foo" }, { value: "bar" }, { value }]; useIdentity(); - const derived = Array.from(arr, mutateAndReturn); + const derived = Array.from(arr).map(mutateAndReturn); let t1; if ($[0] !== derived) { t1 = derived.at(0); @@ -72,7 +72,7 @@ function Component(t0) { export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{ value: 5 }], - sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }, { value: 7 }], }; ``` @@ -80,4 +80,5 @@ export const FIXTURE_ENTRYPOINT = { ### 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 +
{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}
+
{"children":[{"value":"foo","wat0":"joe"},{"value":7,"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 4e224c8a9a..af5bea83a3 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 @@ -3,7 +3,7 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; function Component({value}) { const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; useIdentity(); - const derived = Array.from(arr, mutateAndReturn); + const derived = Array.from(arr).map(mutateAndReturn); return ( {derived.at(0)} @@ -15,5 +15,5 @@ function Component({value}) { export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{value: 5}], - sequentialRenders: [{value: 5}, {value: 6}, {value: 6}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}], }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.expect.md new file mode 100644 index 0000000000..6882f8f4bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +import {useIdentity} from 'shared-runtime'; + +/** + * Forked version of call-spread-argument-mutable-iterator that is known to not mutate + * the spread argument since it is a Set + */ +function useFoo() { + const s = new Set([1, 2]); + useIdentity(null); + return [Math.max(...s), s]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useIdentity } from "shared-runtime"; + +/** + * Forked version of call-spread-argument-mutable-iterator that is known to not mutate + * the spread argument since it is a Set + */ +function useFoo() { + const $ = _c(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = new Set([1, 2]); + $[0] = t0; + } else { + t0 = $[0]; + } + const s = t0; + useIdentity(null); + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [Math.max(...s), s]; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +### Eval output +(kind: ok) [2,{"kind":"Set","value":[1,2]}] +[2,{"kind":"Set","value":[1,2]}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.ts new file mode 100644 index 0000000000..b2746b0168 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/call-spread-argument-set.ts @@ -0,0 +1,17 @@ +import {useIdentity} from 'shared-runtime'; + +/** + * Forked version of call-spread-argument-mutable-iterator that is known to not mutate + * the spread argument since it is a Set + */ +function useFoo() { + const s = new Set([1, 2]); + useIdentity(null); + return [Math.max(...s), s]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{}], + sequentialRenders: [{}, {}], +}; 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 0000000000..61fe33680f --- /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 0000000000..2a0fb6d239 --- /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 0000000000..cb829ffea2 --- /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 0000000000..fe49ba813b --- /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 0000000000..e0d675a2c7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-constructor-arg.expect.md @@ -0,0 +1,108 @@ + +## Input + +```javascript +const MODULE_LOCAL = new Set([4, 5, 6]); +function useFoo({propArr}: {propArr: Array}) { + /* 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]); + + /** + * s4 should be memoized separately from s3 + */ + 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(15); + const { propArr } = t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = [1, 2, 3]; + $[0] = t1; + } else { + t1 = $[0]; + } + let s1; + if ($[1] !== propArr[0]) { + s1 = new Set(t1); + s1.add(propArr[0]); + $[1] = propArr[0]; + $[2] = s1; + } else { + s1 = $[2]; + } + let s2; + let s3; + if ($[3] !== propArr[1] || $[4] !== propArr[2]) { + s2 = new Set(MODULE_LOCAL.values()); + s2.add(propArr[1]); + + s3 = new Set(s2.values()); + s3.add(propArr[2]); + $[3] = propArr[1]; + $[4] = propArr[2]; + $[5] = s2; + $[6] = s3; + } else { + s2 = $[5]; + s3 = $[6]; + } + let s4; + if ($[7] !== propArr[3] || $[8] !== s3) { + s4 = new Set(s3); + s4.add(propArr[3]); + $[7] = propArr[3]; + $[8] = s3; + $[9] = s4; + } else { + s4 = $[9]; + } + let t2; + if ($[10] !== s1 || $[11] !== s2 || $[12] !== s3 || $[13] !== s4) { + t2 = [s1, s2, s3, s4]; + $[10] = s1; + $[11] = s2; + $[12] = s3; + $[13] = s4; + $[14] = t2; + } else { + t2 = $[14]; + } + return t2; +} + +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 0000000000..04508ac175 --- /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}) { + /* 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]); + + /** + * s4 should be memoized separately from s3 + */ + 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 0000000000..371e98089f --- /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 0000000000..049e411d53 --- /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 0000000000..d5fcb7f73d --- /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 0000000000..7bd283371e --- /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 0000000000..47ac8557df --- /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 0000000000..63574c4bc3 --- /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]}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.expect.md new file mode 100644 index 0000000000..0c8fd92816 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(); + const derived = new Set(arr).forEach(mutateAndReturn); + return ( + + {[...derived]} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime"; + +function Component(t0) { + const $ = _c(2); + const { value } = t0; + const arr = [{ value: "foo" }, { value: "bar" }, { value }]; + useIdentity(); + const derived = new Set(arr).forEach(mutateAndReturn); + let t1; + if ($[0] !== derived) { + t1 = {[...derived]}; + $[0] = derived; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 5 }], + sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }, { value: 7 }], +}; + +``` + +### Eval output +(kind: ok) [[ (exception in render) TypeError: derived is not iterable ]] +[[ (exception in render) TypeError: derived is not iterable ]] +[[ (exception in render) TypeError: derived is not iterable ]] +[[ (exception in render) TypeError: derived is not iterable ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.tsx new file mode 100644 index 0000000000..cdab150b63 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/global-types/set-foreach-mutate.tsx @@ -0,0 +1,18 @@ +import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime'; + +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(); + const derived = new Set(arr).forEach(mutateAndReturn); + return ( + + {[...derived]} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 5}], + sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md index ea3f1d4f38..b4aec392e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.expect.md @@ -5,7 +5,7 @@ import {useIdentity, ValidateMemoization} from 'shared-runtime'; /** - * TODO fixture for granular iterator semantics: + * Fixture for granular iterator semantics: * 1. ConditionallyMutate the iterator itself, depending on whether the iterator * is a mutable iterator. * 2. Capture effect on elements within the iterator. @@ -26,7 +26,7 @@ function Validate({x, input}) { function useFoo(input) { 'use memo'; /** - * TODO: We should be able to memoize {} separately from `x`. + * We should be able to memoize {} separately from `x`. */ const x = Array.from([{}]); useIdentity(); @@ -48,7 +48,7 @@ import { c as _c } from "react/compiler-runtime"; import { useIdentity, ValidateMemoization } from "shared-runtime"; /** - * TODO fixture for granular iterator semantics: + * Fixture for granular iterator semantics: * 1. ConditionallyMutate the iterator itself, depending on whether the iterator * is a mutable iterator. * 2. Capture effect on elements within the iterator. @@ -68,29 +68,35 @@ function Validate({ x, input }) { } function useFoo(input) { "use memo"; - const $ = _c(5); - - const x = Array.from([{}]); - useIdentity(); + const $ = _c(6); let t0; - if ($[0] !== input) { - t0 = [input]; - $[0] = input; - $[1] = t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [{}]; + $[0] = t0; } else { - t0 = $[1]; + t0 = $[0]; } - x.push(t0); + const x = Array.from(t0); + useIdentity(); let t1; - if ($[2] !== input || $[3] !== x) { - t1 = ; - $[2] = input; - $[3] = x; - $[4] = t1; + if ($[1] !== input) { + t1 = [input]; + $[1] = input; + $[2] = t1; } else { - t1 = $[4]; + t1 = $[2]; } - return t1; + x.push(t1); + let t2; + if ($[3] !== input || $[4] !== x) { + t2 = ; + $[3] = input; + $[4] = x; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.js index 27d861692c..3e24d0b5b2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-granular-iterator-semantics.js @@ -1,7 +1,7 @@ import {useIdentity, ValidateMemoization} from 'shared-runtime'; /** - * TODO fixture for granular iterator semantics: + * Fixture for granular iterator semantics: * 1. ConditionallyMutate the iterator itself, depending on whether the iterator * is a mutable iterator. * 2. Capture effect on elements within the iterator. @@ -22,7 +22,7 @@ function Validate({x, input}) { function useFoo(input) { 'use memo'; /** - * TODO: We should be able to memoize {} separately from `x`. + * We should be able to memoize {} separately from `x`. */ const x = Array.from([{}]); useIdentity(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-inference-array-from.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-inference-array-from.expect.md index 5209fd953e..ab584c1159 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-inference-array-from.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-inference-array-from.expect.md @@ -77,40 +77,46 @@ function Validate({ x, val1, val2 }) { } function useFoo(t0) { "use memo"; - const $ = _c(8); + const $ = _c(9); const { val1, val2 } = t0; - - const x = Array.from([]); - useIdentity(); let t1; - if ($[0] !== val1) { - t1 = [val1]; - $[0] = val1; - $[1] = t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = []; + $[0] = t1; } else { - t1 = $[1]; + t1 = $[0]; } - x.push(t1); + const x = Array.from(t1); + useIdentity(); let t2; - if ($[2] !== val2) { - t2 = [val2]; - $[2] = val2; - $[3] = t2; + if ($[1] !== val1) { + t2 = [val1]; + $[1] = val1; + $[2] = t2; } else { - t2 = $[3]; + t2 = $[2]; } x.push(t2); let t3; - if ($[4] !== val1 || $[5] !== val2 || $[6] !== x) { - t3 = ; - $[4] = val1; - $[5] = val2; - $[6] = x; - $[7] = t3; + if ($[3] !== val2) { + t3 = [val2]; + $[3] = val2; + $[4] = t3; } else { - t3 = $[7]; + t3 = $[4]; } - return t3; + x.push(t3); + let t4; + if ($[5] !== val1 || $[6] !== val2 || $[7] !== x) { + t4 = ; + $[5] = val1; + $[6] = val2; + $[7] = x; + $[8] = t4; + } else { + t4 = $[8]; + } + return t4; } export const FIXTURE_ENTRYPOINT = {