Skip to content

Commit ce6cdeb

Browse files
committed
[compiler][wip] Environment option for resolving imported module types
Adds a new Environment config option which allows specifying a function that is called to resolve types of imported modules. The function is passed the name of the imported module (the RHS of the import stmt) and can return a TypeConfig, which is a recursive type of the following form: * Object of valid identifier keys (or "*" for wildcard) and values that are TypeConfigs * Function with various properties, whose return type is a TypeConfig * or a reference to a builtin type using one of a small list (currently Ref, Array, MixedReadonly, Primitive) Rather than have to eagerly supply all known types (most of which may not be used) when creating the config, this function can do so lazily. During InferTypes we call `getGlobalDeclaration()` to resolve global types. Originally this was just for known react modules, but if the new config option is passed we also call it to see if it can resolve a type. For `import {name} from 'module'` syntax, we first resolve the module type and then call `getPropertyType(moduleType, 'name')` to attempt to retrieve the property of the module (the module would obviously have to be typed as an object type for this to have a chance of yielding a result). If the module type is returned as null, or the property doesn't exist, we fall through to the original checking of whether the name was hook-like. TODO: * testing * cache the results of modules so we don't have to re-parse/install their types on each LoadGlobal of the same module * decide what to do if the module types are invalid. probably better to fatal rather than bail out, since this would indicate an invalid configuration. ghstack-source-id: 62cb27b Pull Request resolved: #30771
1 parent bf817b5 commit ce6cdeb

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
Global,
1818
GlobalRegistry,
1919
installReAnimatedTypes,
20+
installTypeConfig,
2021
} from './Globals';
2122
import {
2223
BlockId,
@@ -45,6 +46,7 @@ import {
4546
addHook,
4647
} from './ObjectShape';
4748
import {Scope as BabelScope} from '@babel/traverse';
49+
import {TypeSchema} from './TypeSchema';
4850

4951
export const ExternalFunctionSchema = z.object({
5052
// Source for the imported module that exports the `importSpecifierName` functions
@@ -124,6 +126,11 @@ const HookSchema = z.object({
124126

125127
export type Hook = z.infer<typeof HookSchema>;
126128

129+
export const ModuleTypeResolver = z
130+
.function()
131+
.args(z.string())
132+
.returns(z.nullable(TypeSchema));
133+
127134
/*
128135
* TODO(mofeiZ): User defined global types (with corresponding shapes).
129136
* User defined global types should have inline ObjectShapes instead of directly
@@ -137,6 +144,12 @@ export type Hook = z.infer<typeof HookSchema>;
137144
const EnvironmentConfigSchema = z.object({
138145
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
139146

147+
/**
148+
* A function that, given the name of a module, can optionally return a description
149+
* of that module's type signature.
150+
*/
151+
resolveModuleTypeSchema: z.nullable(ModuleTypeResolver).default(null),
152+
140153
/**
141154
* A list of functions which the application compiles as macros, where
142155
* the compiler must ensure they are not compiled to rename the macro or separate the
@@ -736,6 +749,26 @@ export class Environment {
736749
(isHookName(binding.imported) ? this.#getCustomHookType() : null)
737750
);
738751
} else {
752+
const resolveModuleTypeSchema = this.config.resolveModuleTypeSchema;
753+
if (resolveModuleTypeSchema != null) {
754+
const moduleConfig = resolveModuleTypeSchema(binding.module);
755+
if (moduleConfig != null) {
756+
const moduleTypes = TypeSchema.parse(moduleConfig);
757+
const module = installTypeConfig(
758+
this.#globals,
759+
this.#shapes,
760+
moduleTypes,
761+
);
762+
const importedType = this.getPropertyType(
763+
module,
764+
binding.imported,
765+
);
766+
if (importedType != null) {
767+
return importedType;
768+
}
769+
}
770+
}
771+
739772
/**
740773
* For modules we don't own, we look at whether the original name or import alias
741774
* are hook-like. Both of the following are likely hooks so we would return a hook
@@ -758,6 +791,20 @@ export class Environment {
758791
(isHookName(binding.name) ? this.#getCustomHookType() : null)
759792
);
760793
} else {
794+
const resolveModuleTypeSchema = this.config.resolveModuleTypeSchema;
795+
if (resolveModuleTypeSchema != null) {
796+
const moduleConfig = resolveModuleTypeSchema(binding.module);
797+
if (moduleConfig != null) {
798+
const moduleTypes = TypeSchema.parse(moduleConfig);
799+
const module = installTypeConfig(
800+
this.#globals,
801+
this.#shapes,
802+
moduleTypes,
803+
);
804+
// TODO: distinguish handling of import default/namespace
805+
return module;
806+
}
807+
}
761808
return isHookName(binding.name) ? this.#getCustomHookType() : null;
762809
}
763810
}

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {builtinModules} from 'module';
89
import {Effect, ValueKind, ValueReason} from './HIR';
910
import {
1011
BUILTIN_SHAPES,
1112
BuiltInArrayId,
13+
BuiltInMixedReadonlyId,
1214
BuiltInUseActionStateId,
1315
BuiltInUseContextHookId,
1416
BuiltInUseEffectHookId,
@@ -25,6 +27,8 @@ import {
2527
addObject,
2628
} from './ObjectShape';
2729
import {BuiltInType, PolyType} from './Types';
30+
import {TypeConfig} from './TypeSchema';
31+
import {assertExhaustive} from '../Utils/utils';
2832

2933
/*
3034
* This file exports types and defaults for JavaScript global objects.
@@ -528,6 +532,56 @@ DEFAULT_GLOBALS.set(
528532
addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS),
529533
);
530534

535+
export function installTypeConfig(
536+
globals: GlobalRegistry,
537+
shapes: ShapeRegistry,
538+
typeConfig: TypeConfig,
539+
): Global {
540+
switch (typeConfig.kind) {
541+
case 'type': {
542+
switch (typeConfig.name) {
543+
case 'Array': {
544+
return {kind: 'Object', shapeId: BuiltInArrayId};
545+
}
546+
case 'MixedReadonly': {
547+
return {kind: 'Object', shapeId: BuiltInMixedReadonlyId};
548+
}
549+
case 'Primitive': {
550+
return {kind: 'Primitive'};
551+
}
552+
case 'Ref': {
553+
return {kind: 'Object', shapeId: BuiltInUseRefId};
554+
}
555+
default: {
556+
assertExhaustive(
557+
typeConfig.name,
558+
`Unexpected type '${(typeConfig as any).name}'`,
559+
);
560+
}
561+
}
562+
}
563+
case 'function': {
564+
return addFunction(shapes, [], {
565+
positionalParams: typeConfig.positionalParams,
566+
restParam: typeConfig.restParam,
567+
calleeEffect: typeConfig.calleeEffect,
568+
returnType: installTypeConfig(globals, shapes, typeConfig.returnType),
569+
returnValueKind: typeConfig.returnValueKind,
570+
});
571+
}
572+
case 'object': {
573+
return addObject(
574+
shapes,
575+
null,
576+
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => [
577+
key,
578+
installTypeConfig(globals, shapes, value),
579+
]),
580+
);
581+
}
582+
}
583+
}
584+
531585
export function installReAnimatedTypes(
532586
globals: GlobalRegistry,
533587
registry: ShapeRegistry,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {assertExhaustive} from '../Utils/utils';
1212
import {Environment, ReactFunctionType} from './Environment';
1313
import {HookKind} from './ObjectShape';
1414
import {Type, makeType} from './Types';
15+
import {z} from 'zod';
1516

1617
/*
1718
* *******************************************************************************************
@@ -1389,6 +1390,15 @@ export enum Effect {
13891390
Store = 'store',
13901391
}
13911392

1393+
export const EffectSchema = z.enum([
1394+
Effect.Read,
1395+
Effect.Mutate,
1396+
Effect.ConditionallyMutate,
1397+
Effect.Capture,
1398+
Effect.Store,
1399+
Effect.Freeze,
1400+
]);
1401+
13921402
export function isMutableEffect(
13931403
effect: Effect,
13941404
location: SourceLocation,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {isValidIdentifier} from '@babel/types';
9+
import {z} from 'zod';
10+
import {Effect, ValueKind} from '..';
11+
import {EffectSchema} from './HIR';
12+
13+
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
14+
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
15+
.record(
16+
z.string(),
17+
z.lazy(() => TypeSchema),
18+
)
19+
.refine(record => {
20+
return Object.keys(record).every(
21+
key => key === '*' || isValidIdentifier(key),
22+
);
23+
}, 'Expected all "object" property names to be valid identifiers or `*` to match any property');
24+
25+
export type ObjectTypeConfig = {
26+
kind: 'object';
27+
properties: ObjectPropertiesConfig | null;
28+
};
29+
export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
30+
kind: z.literal('object'),
31+
properties: ObjectPropertiesSchema.nullable(),
32+
});
33+
34+
export type FunctionTypeConfig = {
35+
kind: 'function';
36+
positionalParams: Array<Effect>;
37+
restParam: Effect | null;
38+
calleeEffect: Effect;
39+
returnType: TypeConfig;
40+
returnValueKind: ValueKind;
41+
};
42+
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
43+
kind: z.literal('function'),
44+
positionalParams: z.array(EffectSchema),
45+
restParam: EffectSchema.nullable(),
46+
calleeEffect: EffectSchema,
47+
returnType: z.lazy(() => TypeSchema),
48+
returnValueKind: z.nativeEnum(ValueKind),
49+
});
50+
51+
export type BuiltInTypeConfig = 'Ref' | 'Array' | 'Primitive' | 'MixedReadonly';
52+
export const BuiltInTypeSchema: z.ZodType<BuiltInTypeConfig> = z.union([
53+
z.literal('Ref'),
54+
z.literal('Array'),
55+
z.literal('Primitive'),
56+
z.literal('MixedReadonly'),
57+
]);
58+
59+
export type TypeReferenceConfig = {
60+
kind: 'type';
61+
name: BuiltInTypeConfig;
62+
};
63+
export const TypeReferenceSchema: z.ZodType<TypeReferenceConfig> = z.object({
64+
kind: z.literal('type'),
65+
name: BuiltInTypeSchema,
66+
});
67+
68+
export type TypeConfig =
69+
| ObjectTypeConfig
70+
| FunctionTypeConfig
71+
| TypeReferenceConfig;
72+
export const TypeSchema: z.ZodType<TypeConfig> = z.union([
73+
ObjectTypeSchema,
74+
FunctionTypeSchema,
75+
TypeReferenceSchema,
76+
]);

0 commit comments

Comments
 (0)