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