Skip to content

Commit 720aea5

Browse files
committed
[compiler] moduleTypeProvider support for aliasing signatures
This allows us to type things like `nullthrows()` or `identity()` functions where the return type is polymorphic on the input.
1 parent 27b2c46 commit 720aea5

File tree

8 files changed

+515
-2
lines changed

8 files changed

+515
-2
lines changed

compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

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';
916
import {
1017
BUILTIN_SHAPES,
1118
BuiltInArrayId,
@@ -37,10 +44,15 @@ import {
3744
signatureArgument,
3845
} from './ObjectShape';
3946
import {BuiltInType, ObjectType, PolyType} from './Types';
40-
import {TypeConfig} from './TypeSchema';
47+
import {
48+
AliasingEffectConfig,
49+
AliasingSignatureConfig,
50+
TypeConfig,
51+
} from './TypeSchema';
4152
import {assertExhaustive} from '../Utils/utils';
4253
import {isHookName} from './Environment';
4354
import {CompilerError, SourceLocation} from '..';
55+
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
4456

4557
/*
4658
* This file exports types and defaults for JavaScript global objects.
@@ -891,6 +903,10 @@ export function installTypeConfig(
891903
}
892904
}
893905
case 'function': {
906+
const aliasing =
907+
typeConfig.aliasing != null
908+
? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
909+
: null;
894910
return addFunction(shapes, [], {
895911
positionalParams: typeConfig.positionalParams,
896912
restParam: typeConfig.restParam,
@@ -906,9 +922,14 @@ export function installTypeConfig(
906922
noAlias: typeConfig.noAlias === true,
907923
mutableOnlyIfOperandsAreMutable:
908924
typeConfig.mutableOnlyIfOperandsAreMutable === true,
925+
aliasing,
909926
});
910927
}
911928
case 'hook': {
929+
const aliasing =
930+
typeConfig.aliasing != null
931+
? parseAliasingSignatureConfig(typeConfig.aliasing, moduleName, loc)
932+
: null;
912933
return addHook(shapes, {
913934
hookKind: 'Custom',
914935
positionalParams: typeConfig.positionalParams ?? [],
@@ -923,6 +944,7 @@ export function installTypeConfig(
923944
),
924945
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
925946
noAlias: typeConfig.noAlias === true,
947+
aliasing,
926948
});
927949
}
928950
case 'object': {
@@ -965,6 +987,90 @@ export function installTypeConfig(
965987
}
966988
}
967989

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+
9681074
export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
9691075
// hooks that freeze args and return frozen value
9701076
const frozenHooks = [

compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,86 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
3131
properties: ObjectPropertiesSchema.nullable(),
3232
});
3333

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+
34114
export type FunctionTypeConfig = {
35115
kind: 'function';
36116
positionalParams: Array<Effect>;
@@ -42,6 +122,7 @@ export type FunctionTypeConfig = {
42122
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
43123
impure?: boolean | null | undefined;
44124
canonicalName?: string | null | undefined;
125+
aliasing?: AliasingSignatureConfig | null | undefined;
45126
};
46127
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
47128
kind: z.literal('function'),
@@ -54,6 +135,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
54135
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
55136
impure: z.boolean().nullable().optional(),
56137
canonicalName: z.string().nullable().optional(),
138+
aliasing: AliasingSignatureSchema.nullable().optional(),
57139
});
58140

59141
export type HookTypeConfig = {
@@ -63,6 +145,7 @@ export type HookTypeConfig = {
63145
returnType: TypeConfig;
64146
returnValueKind?: ValueKind | null | undefined;
65147
noAlias?: boolean | null | undefined;
148+
aliasing?: AliasingSignatureConfig | null | undefined;
66149
};
67150
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
68151
kind: z.literal('hook'),
@@ -71,6 +154,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
71154
returnType: z.lazy(() => TypeSchema),
72155
returnValueKind: ValueKindSchema.nullable().optional(),
73156
noAlias: z.boolean().nullable().optional(),
157+
aliasing: AliasingSignatureSchema.nullable().optional(),
74158
});
75159

76160
export type BuiltInTypeConfig =
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

0 commit comments

Comments
 (0)