[compiler][optim] infer mixedReadOnly for numeric and computed properties (#32593)
Expand type inference to infer mixedReadOnly types for numeric and computed property accesses. ```js function Component({idx}) const data = useFragment(...) // we want to type `posts` correctly as Array const posts = data.viewers[idx].posts.slice(0, 5); // ... } ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/32593). * #32596 * #32595 * #32594 * __->__ #32593 * #32522 * #32521
mofeiZ committed
Mar 13, 2025 at 11:58 UTC
eb53139ee50fe53c85e8ad51b21ad0968c1f782d
5 files changed
+161
-21
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+34
-9
@@ -1126,9 +1126,32 @@ export class Environment {
1126
);
1127
}
1128
1129
+ getFallthroughPropertyType(
1130
+ receiver: Type,
1131
+ _property: Type,
1132
+ ): BuiltInType | PolyType | null {
1133
+ let shapeId = null;
1134
+ if (receiver.kind === 'Object' || receiver.kind === 'Function') {
1135
+ shapeId = receiver.shapeId;
1136
+ }
1137
+
1138
+ if (shapeId !== null) {
1139
+ const shape = this.#shapes.get(shapeId);
1140
+
1141
+ CompilerError.invariant(shape !== undefined, {
1142
+ reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
1143
+ description: null,
1144
+ loc: null,
1145
+ suggestions: null,
1146
+ });
1147
+ return shape.properties.get('*') ?? null;
1148
+ }
1149
+ return null;
1150
+ }
1151
+
1152
getPropertyType(
1153
receiver: Type,
1131
- property: string,
1154
+ property: string | number,
1155
): BuiltInType | PolyType | null {
1156
let shapeId = null;
1157
if (receiver.kind === 'Object' || receiver.kind === 'Function') {
@@ -1146,17 +1169,19 @@ export class Environment {
1169
loc: null,
1170
suggestions: null,
1171
});
1149
- let value =
1150
- shape.properties.get(property) ?? shape.properties.get('*') ?? null;
1151
- if (value === null && isHookName(property)) {
1152
- value = this.#getCustomHookType();
1172
+ if (typeof property === 'string') {
1173
+ return (
1174
+ shape.properties.get(property) ??
1175
+ shape.properties.get('*') ??
1176
+ (isHookName(property) ? this.#getCustomHookType() : null)
1177
+ );
1178
+ } else {
1179
+ return shape.properties.get('*') ?? null;
1180
}
1154
- return value;
1155
- } else if (isHookName(property)) {
1181
+ } else if (typeof property === 'string' && isHookName(property)) {
1182
return this.#getCustomHookType();
1157
- } else {
1158
- return null;
1183
}
1184
+ return null;
1185
}
1186
1187
getFunctionSignature(type: FunctionType): FunctionSignature | null {
compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts
+9
-1
@@ -60,7 +60,15 @@ export type PropType = {
60
kind: 'Property';
61
objectType: Type;
62
objectName: string;
63
- propertyName: PropertyLiteral;
63
+ propertyName:
64
+ | {
65
+ kind: 'literal';
66
+ value: PropertyLiteral;
67
+ }
68
+ | {
69
+ kind: 'computed';
70
+ value: Type;
71
+ };
72
};
73
74
export type ObjectMethod = {
compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+36
-11
@@ -307,11 +307,26 @@ function* generateInstructionTypes(
307
kind: 'Property',
308
objectType: value.object.identifier.type,
309
objectName: getName(names, value.object.identifier.id),
310
- propertyName: value.property,
310
+ propertyName: {
311
+ kind: 'literal',
312
+ value: value.property,
313
+ },
314
});
315
break;
316
}
317
318
+ case 'ComputedLoad': {
319
+ yield equation(left, {
320
+ kind: 'Property',
321
+ objectType: value.object.identifier.type,
322
+ objectName: getName(names, value.object.identifier.id),
323
+ propertyName: {
324
+ kind: 'computed',
325
+ value: value.property.identifier.type,
326
+ },
327
+ });
328
+ break;
329
+ }
330
case 'MethodCall': {
331
const returnType = makeType();
332
yield equation(value.property.identifier.type, {
@@ -336,7 +351,10 @@ function* generateInstructionTypes(
351
kind: 'Property',
352
objectType: value.value.identifier.type,
353
objectName: getName(names, value.value.identifier.id),
339
- propertyName: makePropertyLiteral(propertyName),
354
+ propertyName: {
355
+ kind: 'literal',
356
+ value: makePropertyLiteral(propertyName),
357
+ },
358
});
359
} else {
360
break;
@@ -353,7 +371,10 @@ function* generateInstructionTypes(
371
kind: 'Property',
372
objectType: value.value.identifier.type,
373
objectName: getName(names, value.value.identifier.id),
356
- propertyName: makePropertyLiteral(property.key.name),
374
+ propertyName: {
375
+ kind: 'literal',
376
+ value: makePropertyLiteral(property.key.name),
377
+ },
378
});
379
}
380
}
@@ -410,7 +431,6 @@ function* generateInstructionTypes(
431
case 'RegExpLiteral':
432
case 'MetaProperty':
433
case 'ComputedStore':
413
- case 'ComputedLoad':
434
case 'Await':
435
case 'GetIterator':
436
case 'IteratorNext':
@@ -454,12 +474,13 @@ class Unifier {
474
return;
475
}
476
const objectType = this.get(tB.objectType);
457
- let propertyType;
458
- if (typeof tB.propertyName === 'number') {
459
- propertyType = null;
460
- } else {
461
- propertyType = this.env.getPropertyType(objectType, tB.propertyName);
462
- }
477
+ const propertyType =
478
+ tB.propertyName.kind === 'literal'
479
+ ? this.env.getPropertyType(objectType, tB.propertyName.value)
480
+ : this.env.getFallthroughPropertyType(
481
+ objectType,
482
+ tB.propertyName.value,
483
+ );
484
if (propertyType !== null) {
485
this.unify(tA, propertyType);
486
}
@@ -677,7 +698,11 @@ class Unifier {
698
const RefLikeNameRE = /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/;
699
700
function isRefLikeName(t: PropType): boolean {
680
- return RefLikeNameRE.test(t.objectName) && t.propertyName === 'current';
701
+ return (
702
+ t.propertyName.kind === 'literal' &&
703
+ RefLikeNameRE.test(t.objectName) &&
704
+ t.propertyName.value === 'current'
705
+ );
706
}
707
708
function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/relay-transitive-mixeddata.expect.md
new
+61
@@ -0,0 +1,61 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {useFragment} from 'shared-runtime';
6
+
7
+/**
8
+ * React compiler should infer that the returned value is a primitive and avoid
9
+ * memoizing it.
10
+ */
11
+function useRelayData({query, idx}) {
12
+ 'use memo';
13
+ const data = useFragment('', query);
14
+ return data.a[idx].toString();
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: useRelayData,
19
+ params: [{query: '', idx: 0}],
20
+ sequentialRenders: [
21
+ {query: '', idx: 0},
22
+ {query: '', idx: 0},
23
+ {query: '', idx: 1},
24
+ ],
25
+};
26
+
27
+```
28
+
29
+## Code
30
+
31
+```javascript
32
+import { useFragment } from "shared-runtime";
33
+
34
+/**
35
+ * React compiler should infer that the returned value is a primitive and avoid
36
+ * memoizing it.
37
+ */
38
+function useRelayData(t0) {
39
+ "use memo";
40
+ const { query, idx } = t0;
41
+
42
+ const data = useFragment("", query);
43
+ return data.a[idx].toString();
44
+}
45
+
46
+export const FIXTURE_ENTRYPOINT = {
47
+ fn: useRelayData,
48
+ params: [{ query: "", idx: 0 }],
49
+ sequentialRenders: [
50
+ { query: "", idx: 0 },
51
+ { query: "", idx: 0 },
52
+ { query: "", idx: 1 },
53
+ ],
54
+};
55
+
56
+```
57
+
58
+### Eval output
59
+(kind: ok) "1"
60
+"1"
61
+"2"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/relay-transitive-mixeddata.js
new
+21
@@ -0,0 +1,21 @@
1
+import {useFragment} from 'shared-runtime';
2
+
3
+/**
4
+ * React compiler should infer that the returned value is a primitive and avoid
5
+ * memoizing it.
6
+ */
7
+function useRelayData({query, idx}) {
8
+ 'use memo';
9
+ const data = useFragment('', query);
10
+ return data.a[idx].toString();
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: useRelayData,
15
+ params: [{query: '', idx: 0}],
16
+ sequentialRenders: [
17
+ {query: '', idx: 0},
18
+ {query: '', idx: 0},
19
+ {query: '', idx: 1},
20
+ ],
21
+};