@samitouri / QOS-React / commits / eb53139ee5

[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 +};