@samitouri / QOS-React-1 / commits / 34179fe344

[compiler] moduleTypeProvider support for aliasing signatures (#33526)

This allows us to type things like `nullthrows()` or `identity()` functions where the return type is polymorphic on the input. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33526). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * __->__ #33526 * #33522 * #33518

Joseph Savona committed Jun 18, 2025 at 15:43 UTC 34179fe3449e141e980bbeaa8fc0a61b156113bb
8 files changed +515 -2
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+108 -2
@@ -5,7 +5,14 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
8 +import {
9 + Effect,
10 + GeneratedSource,
11 + makeIdentifierId,
12 + Place,
13 + ValueKind,
14 + ValueReason,
15 +} from './HIR';
16 import {
17 BUILTIN_SHAPES,
18 BuiltInArrayId,
@@ -37,10 +44,15 @@ import {
44 signatureArgument,
45 } from './ObjectShape';
46 import {BuiltInType, ObjectType, PolyType} from './Types';
40 -import {TypeConfig} from './TypeSchema';
47 +import {
48 + AliasingEffectConfig,
49 + AliasingSignatureConfig,
50 + TypeConfig,
51 +} from './TypeSchema';
52 import {assertExhaustive} from '../Utils/utils';
53 import {isHookName} from './Environment';
54 import {CompilerError, SourceLocation} from '..';
55 +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
56
57 /*
58 * This file exports types and defaults for JavaScript global objects.
@@ -891,6 +903,10 @@ export function installTypeConfig(
903 }
904 }
905 case 'function': {
906 + const aliasing =
907 + typeConfig.aliasing != null
908 + ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
909 + : null;
910 return addFunction(shapes, [], {
911 positionalParams: typeConfig.positionalParams,
912 restParam: typeConfig.restParam,
@@ -906,9 +922,14 @@ export function installTypeConfig(
922 noAlias: typeConfig.noAlias === true,
923 mutableOnlyIfOperandsAreMutable:
924 typeConfig.mutableOnlyIfOperandsAreMutable === true,
925 + aliasing,
926 });
927 }
928 case 'hook': {
929 + const aliasing =
930 + typeConfig.aliasing != null
931 + ? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
932 + : null;
933 return addHook(shapes, {
934 hookKind: 'Custom',
935 positionalParams: typeConfig.positionalParams ?? [],
@@ -923,6 +944,7 @@ export function installTypeConfig(
944 ),
945 returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
946 noAlias: typeConfig.noAlias === true,
947 + aliasing,
948 });
949 }
950 case 'object': {
@@ -965,6 +987,90 @@ export function installTypeConfig(
987 }
988 }
989
990 +function parseAliasingSignatureConfig(
991 + typeConfig: AliasingSignatureConfig,
992 + moduleName: string,
993 + loc: SourceLocation,
994 +): AliasingSignature {
995 + const lifetimes = new Map<string, Place>();
996 + function define(temp: string): Place {
997 + CompilerError.invariant(!lifetimes.has(temp), {
998 + reason: `Invalid type configuration for module`,
999 + description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
1000 + loc,
1001 + });
1002 + const place = signatureArgument(lifetimes.size);
1003 + lifetimes.set(temp, place);
1004 + return place;
1005 + }
1006 + function lookup(temp: string): Place {
1007 + const place = lifetimes.get(temp);
1008 + CompilerError.invariant(place != null, {
1009 + reason: `Invalid type configuration for module`,
1010 + description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
1011 + loc,
1012 + });
1013 + return place;
1014 + }
1015 + const receiver = define(typeConfig.receiver);
1016 + const params = typeConfig.params.map(define);
1017 + const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
1018 + const returns = define(typeConfig.returns);
1019 + const temporaries = typeConfig.temporaries.map(define);
1020 + const effects = typeConfig.effects.map(
1021 + (effect: AliasingEffectConfig): AliasingEffect => {
1022 + switch (effect.kind) {
1023 + case 'Assign': {
1024 + return {
1025 + kind: 'Assign',
1026 + from: lookup(effect.from),
1027 + into: lookup(effect.into),
1028 + };
1029 + }
1030 + case 'Create': {
1031 + return {
1032 + kind: 'Create',
1033 + into: lookup(effect.into),
1034 + reason: ValueReason.KnownReturnSignature,
1035 + value: effect.value,
1036 + };
1037 + }
1038 + case 'Freeze': {
1039 + return {
1040 + kind: 'Freeze',
1041 + value: lookup(effect.value),
1042 + reason: ValueReason.KnownReturnSignature,
1043 + };
1044 + }
1045 + case 'Impure': {
1046 + return {
1047 + kind: 'Impure',
1048 + place: lookup(effect.place),
1049 + error: CompilerError.throwTodo({
1050 + reason: 'Support impure effect declarations',
1051 + loc: GeneratedSource,
1052 + }),
1053 + };
1054 + }
1055 + default: {
1056 + assertExhaustive(
1057 + effect,
1058 + `Unexpected effect kind '${(effect as any).kind}'`,
1059 + );
1060 + }
1061 + }
1062 + },
1063 + );
1064 + return {
1065 + receiver: receiver.identifier.id,
1066 + params: params.map(p => p.identifier.id),
1067 + rest: rest != null ? rest.identifier.id : null,
1068 + returns: returns.identifier.id,
1069 + temporaries,
1070 + effects,
1071 + };
1072 +}
1073 +
1074 export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
1075 // hooks that freeze args and return frozen value
1076 const frozenHooks = [
compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts
+84
@@ -31,6 +31,86 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
31 properties: ObjectPropertiesSchema.nullable(),
32 });
33
34 +export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
35 + message: "Placeholder names must start with '@'",
36 +});
37 +
38 +export type FreezeEffectConfig = {
39 + kind: 'Freeze';
40 + value: string;
41 +};
42 +
43 +export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
44 + kind: z.literal('Freeze'),
45 + value: LifetimeIdSchema,
46 +});
47 +
48 +export type CreateEffectConfig = {
49 + kind: 'Create';
50 + into: string;
51 + value: ValueKind;
52 +};
53 +
54 +export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
55 + kind: z.literal('Create'),
56 + into: LifetimeIdSchema,
57 + value: ValueKindSchema,
58 +});
59 +
60 +export type AssignEffectConfig = {
61 + kind: 'Assign';
62 + from: string;
63 + into: string;
64 +};
65 +
66 +export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
67 + kind: z.literal('Assign'),
68 + from: LifetimeIdSchema,
69 + into: LifetimeIdSchema,
70 +});
71 +
72 +export type ImpureEffectConfig = {
73 + kind: 'Impure';
74 + place: string;
75 +};
76 +
77 +export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
78 + kind: z.literal('Impure'),
79 + place: LifetimeIdSchema,
80 +});
81 +
82 +export type AliasingEffectConfig =
83 + | FreezeEffectConfig
84 + | CreateEffectConfig
85 + | AssignEffectConfig
86 + | ImpureEffectConfig;
87 +
88 +export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
89 + FreezeEffectSchema,
90 + CreateEffectSchema,
91 + AssignEffectSchema,
92 + ImpureEffectSchema,
93 +]);
94 +
95 +export type AliasingSignatureConfig = {
96 + receiver: string;
97 + params: Array<string>;
98 + rest: string | null;
99 + returns: string;
100 + effects: Array<AliasingEffectConfig>;
101 + temporaries: Array<string>;
102 +};
103 +
104 +export const AliasingSignatureSchema: z.ZodType<AliasingSignatureConfig> =
105 + z.object({
106 + receiver: LifetimeIdSchema,
107 + params: z.array(LifetimeIdSchema),
108 + rest: LifetimeIdSchema.nullable(),
109 + returns: LifetimeIdSchema,
110 + effects: z.array(AliasingEffectSchema),
111 + temporaries: z.array(LifetimeIdSchema),
112 + });
113 +
114 export type FunctionTypeConfig = {
115 kind: 'function';
116 positionalParams: Array<Effect>;
@@ -42,6 +122,7 @@ export type FunctionTypeConfig = {
122 mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
123 impure?: boolean | null | undefined;
124 canonicalName?: string | null | undefined;
125 + aliasing?: AliasingSignatureConfig | null | undefined;
126 };
127 export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
128 kind: z.literal('function'),
@@ -54,6 +135,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
135 mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
136 impure: z.boolean().nullable().optional(),
137 canonicalName: z.string().nullable().optional(),
138 + aliasing: AliasingSignatureSchema.nullable().optional(),
139 });
140
141 export type HookTypeConfig = {
@@ -63,6 +145,7 @@ export type HookTypeConfig = {
145 returnType: TypeConfig;
146 returnValueKind?: ValueKind | null | undefined;
147 noAlias?: boolean | null | undefined;
148 + aliasing?: AliasingSignatureConfig | null | undefined;
149 };
150 export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
151 kind: z.literal('hook'),
@@ -71,6 +154,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
154 returnType: z.lazy(() => TypeSchema),
155 returnValueKind: ValueKindSchema.nullable().optional(),
156 noAlias: z.boolean().nullable().optional(),
157 + aliasing: AliasingSignatureSchema.nullable().optional(),
158 });
159
160 export type BuiltInTypeConfig =
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md new
+117
@@ -0,0 +1,117 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +
7 +import {
8 + identity,
9 + makeObject_Primitives,
10 + typedIdentity,
11 + useIdentity,
12 + ValidateMemoization,
13 +} from 'shared-runtime';
14 +
15 +function Component({a, b}) {
16 + // create a mutable value with input `a`
17 + const x = makeObject_Primitives(a);
18 +
19 + // freeze the value
20 + useIdentity(x);
21 +
22 + // known to pass-through via aliasing signature
23 + const x2 = typedIdentity(x);
24 +
25 + // Unknown function so we assume it conditionally mutates,
26 + // but x2 is frozen so this downgrades to a read.
27 + // x should *not* take b as a dependency
28 + identity(x2, b);
29 +
30 + return <ValidateMemoization inputs={[a]} output={x} />;
31 +}
32 +
33 +export const FIXTURE_ENTRYPOINT = {
34 + fn: Component,
35 + params: [{a: 0, b: 0}],
36 + sequentialRenders: [
37 + {a: 0, b: 0},
38 + {a: 1, b: 0},
39 + {a: 1, b: 1},
40 + {a: 0, b: 1},
41 + {a: 0, b: 0},
42 + ],
43 +};
44 +
45 +```
46 +
47 +## Code
48 +
49 +```javascript
50 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
51 +
52 +import {
53 + identity,
54 + makeObject_Primitives,
55 + typedIdentity,
56 + useIdentity,
57 + ValidateMemoization,
58 +} from "shared-runtime";
59 +
60 +function Component(t0) {
61 + const $ = _c(7);
62 + const { a, b } = t0;
63 + let t1;
64 + if ($[0] !== a) {
65 + t1 = makeObject_Primitives(a);
66 + $[0] = a;
67 + $[1] = t1;
68 + } else {
69 + t1 = $[1];
70 + }
71 + const x = t1;
72 +
73 + useIdentity(x);
74 +
75 + const x2 = typedIdentity(x);
76 +
77 + identity(x2, b);
78 + let t2;
79 + if ($[2] !== a) {
80 + t2 = [a];
81 + $[2] = a;
82 + $[3] = t2;
83 + } else {
84 + t2 = $[3];
85 + }
86 + let t3;
87 + if ($[4] !== t2 || $[5] !== x) {
88 + t3 = <ValidateMemoization inputs={t2} output={x} />;
89 + $[4] = t2;
90 + $[5] = x;
91 + $[6] = t3;
92 + } else {
93 + t3 = $[6];
94 + }
95 + return t3;
96 +}
97 +
98 +export const FIXTURE_ENTRYPOINT = {
99 + fn: Component,
100 + params: [{ a: 0, b: 0 }],
101 + sequentialRenders: [
102 + { a: 0, b: 0 },
103 + { a: 1, b: 0 },
104 + { a: 1, b: 1 },
105 + { a: 0, b: 1 },
106 + { a: 0, b: 0 },
107 + ],
108 +};
109 +
110 +```
111 +
112 +### Eval output
113 +(kind: ok) <div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
114 +<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
115 +<div>{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}</div>
116 +<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
117 +<div>{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js new
+39
@@ -0,0 +1,39 @@
1 +// @enableNewMutationAliasingModel
2 +
3 +import {
4 + identity,
5 + makeObject_Primitives,
6 + typedIdentity,
7 + useIdentity,
8 + ValidateMemoization,
9 +} from 'shared-runtime';
10 +
11 +function Component({a, b}) {
12 + // create a mutable value with input `a`
13 + const x = makeObject_Primitives(a);
14 +
15 + // freeze the value
16 + useIdentity(x);
17 +
18 + // known to pass-through via aliasing signature
19 + const x2 = typedIdentity(x);
20 +
21 + // Unknown function so we assume it conditionally mutates,
22 + // but x2 is frozen so this downgrades to a read.
23 + // x should *not* take b as a dependency
24 + identity(x2, b);
25 +
26 + return <ValidateMemoization inputs={[a]} output={x} />;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [{a: 0, b: 0}],
32 + sequentialRenders: [
33 + {a: 0, b: 0},
34 + {a: 1, b: 0},
35 + {a: 1, b: 1},
36 + {a: 0, b: 1},
37 + {a: 0, b: 0},
38 + ],
39 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md new
+112
@@ -0,0 +1,112 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +
7 +import {
8 + identity,
9 + makeObject_Primitives,
10 + typedIdentity,
11 + useIdentity,
12 + ValidateMemoization,
13 +} from 'shared-runtime';
14 +
15 +function Component({a, b}) {
16 + // create a mutable value with input `a`
17 + const x = makeObject_Primitives(a);
18 +
19 + // known to pass-through via aliasing signature
20 + const x2 = typedIdentity(x);
21 +
22 + // Unknown function so we assume it conditionally mutates,
23 + // and x is still mutable so
24 + identity(x2, b);
25 +
26 + return <ValidateMemoization inputs={[a, b]} output={x} />;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [{a: 0, b: 0}],
32 + sequentialRenders: [
33 + {a: 0, b: 0},
34 + {a: 1, b: 0},
35 + {a: 1, b: 1},
36 + {a: 0, b: 1},
37 + {a: 0, b: 0},
38 + ],
39 +};
40 +
41 +```
42 +
43 +## Code
44 +
45 +```javascript
46 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
47 +
48 +import {
49 + identity,
50 + makeObject_Primitives,
51 + typedIdentity,
52 + useIdentity,
53 + ValidateMemoization,
54 +} from "shared-runtime";
55 +
56 +function Component(t0) {
57 + const $ = _c(9);
58 + const { a, b } = t0;
59 + let x;
60 + if ($[0] !== a || $[1] !== b) {
61 + x = makeObject_Primitives(a);
62 +
63 + const x2 = typedIdentity(x);
64 +
65 + identity(x2, b);
66 + $[0] = a;
67 + $[1] = b;
68 + $[2] = x;
69 + } else {
70 + x = $[2];
71 + }
72 + let t1;
73 + if ($[3] !== a || $[4] !== b) {
74 + t1 = [a, b];
75 + $[3] = a;
76 + $[4] = b;
77 + $[5] = t1;
78 + } else {
79 + t1 = $[5];
80 + }
81 + let t2;
82 + if ($[6] !== t1 || $[7] !== x) {
83 + t2 = <ValidateMemoization inputs={t1} output={x} />;
84 + $[6] = t1;
85 + $[7] = x;
86 + $[8] = t2;
87 + } else {
88 + t2 = $[8];
89 + }
90 + return t2;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Component,
95 + params: [{ a: 0, b: 0 }],
96 + sequentialRenders: [
97 + { a: 0, b: 0 },
98 + { a: 1, b: 0 },
99 + { a: 1, b: 1 },
100 + { a: 0, b: 1 },
101 + { a: 0, b: 0 },
102 + ],
103 +};
104 +
105 +```
106 +
107 +### Eval output
108 +(kind: ok) <div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
109 +<div>{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}</div>
110 +<div>{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}</div>
111 +<div>{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}</div>
112 +<div>{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js new
+35
@@ -0,0 +1,35 @@
1 +// @enableNewMutationAliasingModel
2 +
3 +import {
4 + identity,
5 + makeObject_Primitives,
6 + typedIdentity,
7 + useIdentity,
8 + ValidateMemoization,
9 +} from 'shared-runtime';
10 +
11 +function Component({a, b}) {
12 + // create a mutable value with input `a`
13 + const x = makeObject_Primitives(a);
14 +
15 + // known to pass-through via aliasing signature
16 + const x2 = typedIdentity(x);
17 +
18 + // Unknown function so we assume it conditionally mutates,
19 + // and x is still mutable so
20 + identity(x2, b);
21 +
22 + return <ValidateMemoization inputs={[a, b]} output={x} />;
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{a: 0, b: 0}],
28 + sequentialRenders: [
29 + {a: 0, b: 0},
30 + {a: 1, b: 0},
31 + {a: 1, b: 1},
32 + {a: 0, b: 1},
33 + {a: 0, b: 0},
34 + ],
35 +};
compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts
+16
@@ -69,6 +69,22 @@ export function makeSharedRuntimeTypeProvider({
69 returnValueKind: ValueKindEnum.Mutable,
70 noAlias: true,
71 },
72 + typedIdentity: {
73 + kind: 'function',
74 + positionalParams: [EffectEnum.Read],
75 + restParam: null,
76 + calleeEffect: EffectEnum.Read,
77 + returnType: {kind: 'type', name: 'Any'},
78 + returnValueKind: ValueKindEnum.Mutable,
79 + aliasing: {
80 + receiver: '@receiver',
81 + params: ['@value'],
82 + rest: null,
83 + returns: '@return',
84 + temporaries: [],
85 + effects: [{kind: 'Assign', from: '@value', into: '@return'}],
86 + },
87 + },
88 },
89 };
90 } else if (moduleName === 'ReactCompilerTest') {
compiler/packages/snap/src/sprout/shared-runtime.ts
+4
@@ -396,4 +396,8 @@ export function typedLog(...values: Array<any>): void {
396 console.log(...values);
397 }
398
399 +export function typedIdentity<T>(value: T): T {
400 + return value;
401 +}
402 +
403 export default typedLog;