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 89d1529180..561bdab698 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -114,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [ returnValueKind: ValueKind.Mutable, }), ], + [ + 'entries', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'keys', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Read], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Only keys are captured, and keys are immutable + { + kind: 'ImmutableCapture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], + [ + 'values', + addFunction(DEFAULT_SHAPES, [], { + positionalParams: [Effect.Capture], + restParam: null, + returnType: {kind: 'Object', shapeId: BuiltInArrayId}, + calleeEffect: Effect.Read, + returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@object'], + rest: null, + returns: '@returns', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@returns', + reason: ValueReason.KnownReturnSignature, + value: ValueKind.Mutable, + }, + // Object values are captured into the return + { + kind: 'Capture', + from: '@object', + into: '@returns', + }, + ], + }, + }), + ], ]), ], [ 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 b20bebbae3..2c626243e7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -142,6 +142,7 @@ function parseAliasingSignatureConfig( const effects = typeConfig.effects.map( (effect: AliasingEffectConfig): AliasingEffect => { switch (effect.kind) { + case 'ImmutableCapture': case 'CreateFrom': case 'Capture': case 'Alias': diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 8f28a1357b..42c7d2d89d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType = z.object({ into: LifetimeIdSchema, }); +export type ImmutableCaptureEffectConfig = { + kind: 'ImmutableCapture'; + from: string; + into: string; +}; + +export const ImmutableCaptureEffectSchema: z.ZodType = + z.object({ + kind: z.literal('ImmutableCapture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + export type CaptureEffectConfig = { kind: 'Capture'; from: string; @@ -187,6 +200,7 @@ export type AliasingEffectConfig = | AssignEffectConfig | AliasEffectConfig | CaptureEffectConfig + | ImmutableCaptureEffectConfig | ImpureEffectConfig | MutateEffectConfig | MutateTransitiveConditionallyConfig @@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType = z.union([ AssignEffectSchema, AliasEffectSchema, CaptureEffectSchema, + ImmutableCaptureEffectSchema, ImpureEffectSchema, MutateEffectSchema, MutateTransitiveConditionallySchema, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md new file mode 100644 index 0000000000..f076d9032d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.expect.md @@ -0,0 +1,108 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(7); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 = Object.keys(record); + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== record || $[5] !== t2) { + t3 = ( +
+ {t2.map((id) => ( + + ))} +
+ ); + $[4] = record; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} +function _temp(item) { + return [item.id, (ref) => ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js new file mode 100644 index 0000000000..38ae97ab95 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-keys.js @@ -0,0 +1,36 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.keys(record).map(id => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md new file mode 100644 index 0000000000..9003990664 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.expect.md @@ -0,0 +1,103 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(4); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 =
{Object.values(record).map(_temp2)}
; + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp2(t0) { + const { id, render } = t0; + return ; +} +function _temp(item) { + return [ + item.id, + { id: item.id, render: (ref) => }, + ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js new file mode 100644 index 0000000000..4cf229c379 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-values.js @@ -0,0 +1,39 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [ + item.id, + {id: item.id, render: ref => }, + ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.values(record).map(({id, render}) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md new file mode 100644 index 0000000000..b7ebcbc1cf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.expect.md @@ -0,0 +1,97 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.entries(record).map(([id, render]) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useMemo } from "react"; +import { Stringify } from "shared-runtime"; + +// derived from https://github.com/facebook/react/issues/32261 +function Component(t0) { + const $ = _c(4); + const { items } = t0; + let t1; + if ($[0] !== items) { + t1 = Object.fromEntries(items.map(_temp)); + $[0] = items; + $[1] = t1; + } else { + t1 = $[1]; + } + const record = t1; + let t2; + if ($[2] !== record) { + t2 =
{Object.entries(record).map(_temp2)}
; + $[2] = record; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp2(t0) { + const [id, render] = t0; + return ; +} +function _temp(item) { + return [item.id, (ref) => ]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + { id: "0", name: "Hello" }, + { id: "1", name: "World!" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"render":"[[ function params=1 ]]"}
{"render":"[[ function params=1 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js new file mode 100644 index 0000000000..7dd8f34826 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-fromEntries-entries.js @@ -0,0 +1,36 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useMemo} from 'react'; +import {Stringify} from 'shared-runtime'; + +// derived from https://github.com/facebook/react/issues/32261 +function Component({items}) { + const record = useMemo( + () => + Object.fromEntries( + items.map(item => [item.id, ref => ]) + ), + [items] + ); + + // Without a declaration for Object.entries(), this would be assumed to mutate + // `record`, meaning existing memoization couldn't be preserved + return ( +
+ {Object.entries(record).map(([id, render]) => ( + + ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [ + { + items: [ + {id: '0', name: 'Hello'}, + {id: '1', name: 'World!'}, + ], + }, + ], +};