Skip to content

Commit 0860b9c

Browse files
authored
[compiler] Add definitions for Object entries/keys/values (#34047)
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. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34047). * #34049 * __->__ #34047 * #34044
1 parent 538ac7a commit 0860b9c

17 files changed

+818
-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,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
7+
8+
function Component(props) {
9+
const object = {object: props.object};
10+
const entries = useMemo(() => Object.entries(object), [object]);
11+
entries.map(([, value]) => {
12+
value.updated = true;
13+
});
14+
return <Stringify entries={entries} />;
15+
}
16+
17+
export const FIXTURE_ENTRYPOINT = {
18+
fn: Component,
19+
params: [{object: {key: makeObject_Primitives()}}],
20+
};
21+
22+
```
23+
24+
25+
## Error
26+
27+
```
28+
Found 2 errors:
29+
30+
Memoization: Compilation skipped because existing memoization could not be preserved
31+
32+
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
33+
34+
error.validate-object-entries-mutation.ts:6:57
35+
4 | function Component(props) {
36+
5 | const object = {object: props.object};
37+
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
38+
| ^^^^^^ This dependency may be modified later
39+
7 | entries.map(([, value]) => {
40+
8 | value.updated = true;
41+
9 | });
42+
43+
Memoization: Compilation skipped because existing memoization could not be preserved
44+
45+
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
46+
47+
error.validate-object-entries-mutation.ts:6:18
48+
4 | function Component(props) {
49+
5 | const object = {object: props.object};
50+
> 6 | const entries = useMemo(() => Object.entries(object), [object]);
51+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
52+
7 | entries.map(([, value]) => {
53+
8 | value.updated = true;
54+
9 | });
55+
```
56+
57+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
3+
4+
function Component(props) {
5+
const object = {object: props.object};
6+
const entries = useMemo(() => Object.entries(object), [object]);
7+
entries.map(([, value]) => {
8+
value.updated = true;
9+
});
10+
return <Stringify entries={entries} />;
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: Component,
15+
params: [{object: {key: makeObject_Primitives()}}],
16+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validatePreserveExistingMemoizationGuarantees
6+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
7+
8+
function Component(props) {
9+
const object = {object: props.object};
10+
const values = useMemo(() => Object.values(object), [object]);
11+
values.map(value => {
12+
value.updated = true;
13+
});
14+
return <Stringify values={values} />;
15+
}
16+
17+
export const FIXTURE_ENTRYPOINT = {
18+
fn: Component,
19+
params: [{object: {key: makeObject_Primitives()}}],
20+
};
21+
22+
```
23+
24+
25+
## Error
26+
27+
```
28+
Found 2 errors:
29+
30+
Memoization: Compilation skipped because existing memoization could not be preserved
31+
32+
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly.
33+
34+
error.validate-object-values-mutation.ts:6:55
35+
4 | function Component(props) {
36+
5 | const object = {object: props.object};
37+
> 6 | const values = useMemo(() => Object.values(object), [object]);
38+
| ^^^^^^ This dependency may be modified later
39+
7 | values.map(value => {
40+
8 | value.updated = true;
41+
9 | });
42+
43+
Memoization: Compilation skipped because existing memoization could not be preserved
44+
45+
React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.
46+
47+
error.validate-object-values-mutation.ts:6:17
48+
4 | function Component(props) {
49+
5 | const object = {object: props.object};
50+
> 6 | const values = useMemo(() => Object.values(object), [object]);
51+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not preserve existing memoization
52+
7 | values.map(value => {
53+
8 | value.updated = true;
54+
9 | });
55+
```
56+
57+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @validatePreserveExistingMemoizationGuarantees
2+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
3+
4+
function Component(props) {
5+
const object = {object: props.object};
6+
const values = useMemo(() => Object.values(object), [object]);
7+
values.map(value => {
8+
value.updated = true;
9+
});
10+
return <Stringify values={values} />;
11+
}
12+
13+
export const FIXTURE_ENTRYPOINT = {
14+
fn: Component,
15+
params: [{object: {key: makeObject_Primitives()}}],
16+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
6+
7+
function Component(props) {
8+
const object = {object: props.object};
9+
const entries = Object.entries(object);
10+
entries.map(([, value]) => {
11+
value.updated = true;
12+
});
13+
return <Stringify entries={entries} />;
14+
}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: Component,
18+
params: [{object: {key: makeObject_Primitives()}}],
19+
};
20+
21+
```
22+
23+
## Code
24+
25+
```javascript
26+
import { c as _c } from "react/compiler-runtime";
27+
import { makeObject_Primitives, Stringify } from "shared-runtime";
28+
29+
function Component(props) {
30+
const $ = _c(2);
31+
let t0;
32+
if ($[0] !== props.object) {
33+
const object = { object: props.object };
34+
const entries = Object.entries(object);
35+
entries.map(_temp);
36+
t0 = <Stringify entries={entries} />;
37+
$[0] = props.object;
38+
$[1] = t0;
39+
} else {
40+
t0 = $[1];
41+
}
42+
return t0;
43+
}
44+
function _temp(t0) {
45+
const [, value] = t0;
46+
value.updated = true;
47+
}
48+
49+
export const FIXTURE_ENTRYPOINT = {
50+
fn: Component,
51+
params: [{ object: { key: makeObject_Primitives() } }],
52+
};
53+
54+
```
55+
56+
### Eval output
57+
(kind: ok) <div>{"entries":[["object",{"key":{"a":0,"b":"value1","c":true},"updated":true}]]}</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {makeObject_Primitives, Stringify} from 'shared-runtime';
2+
3+
function Component(props) {
4+
const object = {object: props.object};
5+
const entries = Object.entries(object);
6+
entries.map(([, value]) => {
7+
value.updated = true;
8+
});
9+
return <Stringify entries={entries} />;
10+
}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: Component,
14+
params: [{object: {key: makeObject_Primitives()}}],
15+
};

0 commit comments

Comments
 (0)