Skip to content

Commit bfd53a3

Browse files
committed
[compiler] Add definitions for Object entries/keys/values
Fixes remaining issue in #32261, where passing a previously useMemo()-d value to `Object.entries()` makes the compiler think the value is mutated and fail validatePreserveExistingMemo. While I was there I added Object.keys() and Object.values() too.
1 parent 7d696dc commit bfd53a3

File tree

9 files changed

+528
-0
lines changed

9 files changed

+528
-0
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,99 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
114114
returnValueKind: ValueKind.Mutable,
115115
}),
116116
],
117+
[
118+
'entries',
119+
addFunction(DEFAULT_SHAPES, [], {
120+
positionalParams: [Effect.Capture],
121+
restParam: null,
122+
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
123+
calleeEffect: Effect.Read,
124+
returnValueKind: ValueKind.Mutable,
125+
aliasing: {
126+
receiver: '@receiver',
127+
params: ['@object'],
128+
rest: null,
129+
returns: '@returns',
130+
temporaries: [],
131+
effects: [
132+
{
133+
kind: 'Create',
134+
into: '@returns',
135+
reason: ValueReason.KnownReturnSignature,
136+
value: ValueKind.Mutable,
137+
},
138+
// Object values are captured into the return
139+
{
140+
kind: 'Capture',
141+
from: '@object',
142+
into: '@returns',
143+
},
144+
],
145+
},
146+
}),
147+
],
148+
[
149+
'keys',
150+
addFunction(DEFAULT_SHAPES, [], {
151+
positionalParams: [Effect.Read],
152+
restParam: null,
153+
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
154+
calleeEffect: Effect.Read,
155+
returnValueKind: ValueKind.Mutable,
156+
aliasing: {
157+
receiver: '@receiver',
158+
params: ['@object'],
159+
rest: null,
160+
returns: '@returns',
161+
temporaries: [],
162+
effects: [
163+
{
164+
kind: 'Create',
165+
into: '@returns',
166+
reason: ValueReason.KnownReturnSignature,
167+
value: ValueKind.Mutable,
168+
},
169+
// Only keys are captured, and keys are immutable
170+
{
171+
kind: 'ImmutableCapture',
172+
from: '@object',
173+
into: '@returns',
174+
},
175+
],
176+
},
177+
}),
178+
],
179+
[
180+
'values',
181+
addFunction(DEFAULT_SHAPES, [], {
182+
positionalParams: [Effect.Capture],
183+
restParam: null,
184+
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
185+
calleeEffect: Effect.Read,
186+
returnValueKind: ValueKind.Mutable,
187+
aliasing: {
188+
receiver: '@receiver',
189+
params: ['@object'],
190+
rest: null,
191+
returns: '@returns',
192+
temporaries: [],
193+
effects: [
194+
{
195+
kind: 'Create',
196+
into: '@returns',
197+
reason: ValueReason.KnownReturnSignature,
198+
value: ValueKind.Mutable,
199+
},
200+
// Object values are captured into the return
201+
{
202+
kind: 'Capture',
203+
from: '@object',
204+
into: '@returns',
205+
},
206+
],
207+
},
208+
}),
209+
],
117210
]),
118211
],
119212
[

compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ function parseAliasingSignatureConfig(
142142
const effects = typeConfig.effects.map(
143143
(effect: AliasingEffectConfig): AliasingEffect => {
144144
switch (effect.kind) {
145+
case 'ImmutableCapture':
145146
case 'CreateFrom':
146147
case 'Capture':
147148
case 'Alias':

compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
111111
into: LifetimeIdSchema,
112112
});
113113

114+
export type ImmutableCaptureEffectConfig = {
115+
kind: 'ImmutableCapture';
116+
from: string;
117+
into: string;
118+
};
119+
120+
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
121+
z.object({
122+
kind: z.literal('ImmutableCapture'),
123+
from: LifetimeIdSchema,
124+
into: LifetimeIdSchema,
125+
});
126+
114127
export type CaptureEffectConfig = {
115128
kind: 'Capture';
116129
from: string;
@@ -187,6 +200,7 @@ export type AliasingEffectConfig =
187200
| AssignEffectConfig
188201
| AliasEffectConfig
189202
| CaptureEffectConfig
203+
| ImmutableCaptureEffectConfig
190204
| ImpureEffectConfig
191205
| MutateEffectConfig
192206
| MutateTransitiveConditionallyConfig
@@ -199,6 +213,7 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
199213
AssignEffectSchema,
200214
AliasEffectSchema,
201215
CaptureEffectSchema,
216+
ImmutableCaptureEffectSchema,
202217
ImpureEffectSchema,
203218
MutateEffectSchema,
204219
MutateTransitiveConditionallySchema,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import {useMemo} from 'react';
7+
import {Stringify} from 'shared-runtime';
8+
9+
// derived from https://github.com/facebook/react/issues/32261
10+
function Component({items}) {
11+
const record = useMemo(
12+
() =>
13+
Object.fromEntries(
14+
items.map(item => [item.id, ref => <Stringify ref={ref} {...item} />])
15+
),
16+
[items]
17+
);
18+
19+
// Without a declaration for Object.entries(), this would be assumed to mutate
20+
// `record`, meaning existing memoization couldn't be preserved
21+
return (
22+
<div>
23+
{Object.keys(record).map(id => (
24+
<Stringify key={id} render={record[id]} />
25+
))}
26+
</div>
27+
);
28+
}
29+
30+
export const FIXTURE_ENTRYPOINT = {
31+
fn: Component,
32+
params: [
33+
{
34+
items: [
35+
{id: '0', name: 'Hello'},
36+
{id: '1', name: 'World!'},
37+
],
38+
},
39+
],
40+
};
41+
42+
```
43+
44+
## Code
45+
46+
```javascript
47+
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
48+
import { useMemo } from "react";
49+
import { Stringify } from "shared-runtime";
50+
51+
// derived from https://github.com/facebook/react/issues/32261
52+
function Component(t0) {
53+
const $ = _c(7);
54+
const { items } = t0;
55+
let t1;
56+
if ($[0] !== items) {
57+
t1 = Object.fromEntries(items.map(_temp));
58+
$[0] = items;
59+
$[1] = t1;
60+
} else {
61+
t1 = $[1];
62+
}
63+
const record = t1;
64+
let t2;
65+
if ($[2] !== record) {
66+
t2 = Object.keys(record);
67+
$[2] = record;
68+
$[3] = t2;
69+
} else {
70+
t2 = $[3];
71+
}
72+
let t3;
73+
if ($[4] !== record || $[5] !== t2) {
74+
t3 = (
75+
<div>
76+
{t2.map((id) => (
77+
<Stringify key={id} render={record[id]} />
78+
))}
79+
</div>
80+
);
81+
$[4] = record;
82+
$[5] = t2;
83+
$[6] = t3;
84+
} else {
85+
t3 = $[6];
86+
}
87+
return t3;
88+
}
89+
function _temp(item) {
90+
return [item.id, (ref) => <Stringify ref={ref} {...item} />];
91+
}
92+
93+
export const FIXTURE_ENTRYPOINT = {
94+
fn: Component,
95+
params: [
96+
{
97+
items: [
98+
{ id: "0", name: "Hello" },
99+
{ id: "1", name: "World!" },
100+
],
101+
},
102+
],
103+
};
104+
105+
```
106+
107+
### Eval output
108+
(kind: ok) <div><div>{"render":"[[ function params=1 ]]"}</div><div>{"render":"[[ function params=1 ]]"}</div></div>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
import {useMemo} from 'react';
3+
import {Stringify} from 'shared-runtime';
4+
5+
// derived from https://github.com/facebook/react/issues/32261
6+
function Component({items}) {
7+
const record = useMemo(
8+
() =>
9+
Object.fromEntries(
10+
items.map(item => [item.id, ref => <Stringify ref={ref} {...item} />])
11+
),
12+
[items]
13+
);
14+
15+
// Without a declaration for Object.entries(), this would be assumed to mutate
16+
// `record`, meaning existing memoization couldn't be preserved
17+
return (
18+
<div>
19+
{Object.keys(record).map(id => (
20+
<Stringify key={id} render={record[id]} />
21+
))}
22+
</div>
23+
);
24+
}
25+
26+
export const FIXTURE_ENTRYPOINT = {
27+
fn: Component,
28+
params: [
29+
{
30+
items: [
31+
{id: '0', name: 'Hello'},
32+
{id: '1', name: 'World!'},
33+
],
34+
},
35+
],
36+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import {useMemo} from 'react';
7+
import {Stringify} from 'shared-runtime';
8+
9+
// derived from https://github.com/facebook/react/issues/32261
10+
function Component({items}) {
11+
const record = useMemo(
12+
() =>
13+
Object.fromEntries(
14+
items.map(item => [
15+
item.id,
16+
{id: item.id, render: ref => <Stringify ref={ref} {...item} />},
17+
])
18+
),
19+
[items]
20+
);
21+
22+
// Without a declaration for Object.entries(), this would be assumed to mutate
23+
// `record`, meaning existing memoization couldn't be preserved
24+
return (
25+
<div>
26+
{Object.values(record).map(({id, render}) => (
27+
<Stringify key={id} render={render} />
28+
))}
29+
</div>
30+
);
31+
}
32+
33+
export const FIXTURE_ENTRYPOINT = {
34+
fn: Component,
35+
params: [
36+
{
37+
items: [
38+
{id: '0', name: 'Hello'},
39+
{id: '1', name: 'World!'},
40+
],
41+
},
42+
],
43+
};
44+
45+
```
46+
47+
## Code
48+
49+
```javascript
50+
import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
51+
import { useMemo } from "react";
52+
import { Stringify } from "shared-runtime";
53+
54+
// derived from https://github.com/facebook/react/issues/32261
55+
function Component(t0) {
56+
const $ = _c(4);
57+
const { items } = t0;
58+
let t1;
59+
if ($[0] !== items) {
60+
t1 = Object.fromEntries(items.map(_temp));
61+
$[0] = items;
62+
$[1] = t1;
63+
} else {
64+
t1 = $[1];
65+
}
66+
const record = t1;
67+
let t2;
68+
if ($[2] !== record) {
69+
t2 = <div>{Object.values(record).map(_temp2)}</div>;
70+
$[2] = record;
71+
$[3] = t2;
72+
} else {
73+
t2 = $[3];
74+
}
75+
return t2;
76+
}
77+
function _temp2(t0) {
78+
const { id, render } = t0;
79+
return <Stringify key={id} render={render} />;
80+
}
81+
function _temp(item) {
82+
return [
83+
item.id,
84+
{ id: item.id, render: (ref) => <Stringify ref={ref} {...item} /> },
85+
];
86+
}
87+
88+
export const FIXTURE_ENTRYPOINT = {
89+
fn: Component,
90+
params: [
91+
{
92+
items: [
93+
{ id: "0", name: "Hello" },
94+
{ id: "1", name: "World!" },
95+
],
96+
},
97+
],
98+
};
99+
100+
```
101+
102+
### Eval output
103+
(kind: ok) <div><div>{"render":"[[ function params=1 ]]"}</div><div>{"render":"[[ function params=1 ]]"}</div></div>

0 commit comments

Comments
 (0)