Skip to content

[compiler] Custom type definitions in config #30670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 107 additions & 11 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DEFAULT_SHAPES,
Global,
GlobalRegistry,
installCustomGlobals,
installReAnimatedTypes,
} from './Globals';
import {
Expand Down Expand Up @@ -125,18 +126,76 @@ const HookSchema = z.object({
export type Hook = z.infer<typeof HookSchema>;

/*
* TODO(mofeiZ): User defined global types (with corresponding shapes).
* User defined global types should have inline ObjectShapes instead of directly
* using ObjectShapes.ShapeRegistry, as a user-provided ShapeRegistry may be
* accidentally be not well formed.
* i.e.
* missing required shapes (BuiltInArray for [] and BuiltInObject for {})
* missing some recursive Object / Function shapeIds
* Extremely hacky Zod typing in order to get recursive types working for
* external type schema definitions.
*/

const EffectSchema = z.enum([
Effect.Read,
Effect.Mutate,
Effect.ConditionallyMutate,
Effect.Capture,
Effect.Store,
Effect.Freeze,
]);

const TypeIDSchema = z.number();

const TypeReferenceSchema = z.union([
TypeIDSchema,
z.literal('array'),
z.literal('ref'),
]);

const FunctionTypeSchema = z.object({
positionalParams: z.array(EffectSchema),
restParam: EffectSchema.nullable(),
calleeEffect: EffectSchema,
returnType: TypeReferenceSchema.nullable(),
returnValueKind: z.nativeEnum(ValueKind),
});

const baseTypeSchema = z.object({
id: TypeIDSchema.nullable().default(null),
fn: FunctionTypeSchema.nullable(),
});

export type ZodCompositeType<
T extends z.ZodTypeAny,
U extends {[K in keyof U]: z.ZodType<any, z.ZodTypeDef, any>},
> = z.ZodType<
z.output<T> & {[K in keyof U]: z.output<U[K]>},
z.ZodTypeDef,
z.input<T> & {[K in keyof U]: z.input<U[K]>}
>;

export const TypeSchema: ZodCompositeType<
typeof baseTypeSchema,
{
properties: typeof propertiesField;
}
> = baseTypeSchema.extend({
properties: z.lazy(() => propertiesField),
});

const propertiesField = z.array(z.tuple([z.string(), TypeSchema]));

export type GlobalEffect = z.infer<typeof EffectSchema>;

export type GlobalType = z.infer<typeof baseTypeSchema> & {
properties: Array<[string, GlobalType]>;
};

export type GlobalFunctionType = z.infer<typeof FunctionTypeSchema>;

const EnvironmentConfigSchema = z.object({
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),

typedGlobals: z
.array(z.tuple([z.string(), TypeSchema]))
.optional()
.default([]),

/**
* A list of functions which the application compiles as macros, where
* the compiler must ensure they are not compiled to rename the macro or separate the
Expand Down Expand Up @@ -520,12 +579,46 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
props.push({type: 'name', name: elt});
}
}
console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
maybeConfig[key] = [[valSplit[0], props]];
}
continue;
}

if (key === 'customType') {
maybeConfig['typedGlobals'] = [
[
'custom',
{
id: null,
fn: {
positionalParams: [],
restParam: 'read' as Effect.Read,
calleeEffect: 'read' as Effect.Read,
returnType: null,
returnValueKind: 'primitive' as ValueKind.Primitive,
},
properties: [
[
'prop',
{
id: null,
fn: {
positionalParams: [],
restParam: 'read' as Effect.Read,
calleeEffect: 'read' as Effect.Read,
returnType: null,
returnValueKind: 'primitive' as ValueKind.Primitive,
},
properties: [],
},
],
],
},
],
];
continue;
}

if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
// skip parsing non-boolean properties
continue;
Expand Down Expand Up @@ -650,6 +743,8 @@ export class Environment {
installReAnimatedTypes(this.#globals, this.#shapes);
}

installCustomGlobals(this.#globals, this.#shapes, this.config.typedGlobals);

this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
}
Expand Down Expand Up @@ -717,7 +812,7 @@ export class Environment {
);
}
case 'ImportSpecifier': {
if (this.#isKnownReactModule(binding.module)) {
if (this.#isKnownTypedModule(binding.module)) {
/**
* For `import {imported as name} from "..."` form, we use the `imported`
* name rather than the local alias. Because we don't have definitions for
Expand Down Expand Up @@ -745,7 +840,7 @@ export class Environment {
}
case 'ImportDefault':
case 'ImportNamespace': {
if (this.#isKnownReactModule(binding.module)) {
if (this.#isKnownTypedModule(binding.module)) {
// only resolve imports to modules we know about
return (
this.#globals.get(binding.name) ??
Expand All @@ -758,10 +853,11 @@ export class Environment {
}
}

#isKnownReactModule(moduleName: string): boolean {
#isKnownTypedModule(moduleName: string): boolean {
return (
moduleName.toLowerCase() === 'react' ||
moduleName.toLowerCase() === 'react-dom' ||
this.#globals.has(moduleName) ||
(this.config.enableSharedRuntime__testonly &&
moduleName === 'shared-runtime')
);
Expand Down
67 changes: 67 additions & 0 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/

import {CompilerError} from '..';
import {GlobalType} from './Environment';
import {Effect, ValueKind, ValueReason} from './HIR';
import {
BUILTIN_SHAPES,
Expand All @@ -19,6 +21,7 @@ import {
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
FunctionSignature,
ShapeRegistry,
addFunction,
addHook,
Expand Down Expand Up @@ -528,6 +531,70 @@ DEFAULT_GLOBALS.set(
addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS),
);

export function installCustomGlobals(
globals: GlobalRegistry,
registry: ShapeRegistry,
custom: Array<[string, GlobalType]>,
): void {
const refMap: Map<number, BuiltInType> = new Map();
const toSet: Map<number, (ty: BuiltInType) => void> = new Map();
function installShape(name: string, type: GlobalType): BuiltInType {
const props: Array<[string, BuiltInType]> = type.properties.map(
([name, type]) => [name, installShape(name, type)],
);

let ty;
if (type.fn == null) {
ty = addObject(registry, name, props);
} else {
const func: Omit<FunctionSignature, 'hookKind'> = {
positionalParams: type.fn.positionalParams,
restParam: type.fn.restParam,
calleeEffect: type.fn.calleeEffect,
returnValueKind: type.fn.returnValueKind,
returnType: {kind: 'Primitive'}, // Placeholder, mutated below
};

if (type.fn.returnType == null) {
func.returnType = {kind: 'Poly'};
} else if (type.fn.returnType === 'array') {
func.returnType = {kind: 'Object', shapeId: BuiltInArrayId};
} else if (type.fn.returnType === 'ref') {
func.returnType = {kind: 'Object', shapeId: BuiltInUseRefId};
} else if (refMap.has(type.fn.returnType)) {
func.returnType = refMap.get(type.fn.returnType)!;
} else {
const cur = toSet.get(type.fn.returnType);
toSet.set(type.fn.returnType, (ty: BuiltInType) => {
cur?.(ty);
func.returnType = ty;
});
}
ty = addFunction(registry, props, func, name);
}

if (type.id != null) {
CompilerError.invariant(refMap.get(type.id) == null, {
reason: 'Duplicate type id',
loc: null,
});
refMap.set(type.id, ty);
toSet.get(type.id)?.(ty);
toSet.delete(type.id);
}
return ty;
}

for (const [name, type] of custom) {
globals.set(name, installShape(name, type));
}

CompilerError.invariant(toSet.size === 0, {
reason: 'Unresolved type ids',
loc: null,
});
}

export function installReAnimatedTypes(
globals: GlobalRegistry,
registry: ShapeRegistry,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

## Input

```javascript
// @customType

import custom from 'custom';

function Component(props) {
const x = [props.x];
const y = [props.y];

useHook();

custom(x);
custom.prop(x);
custom.notPresent(y);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this case handled? we don't have a declaration for it so i would expect it to assume it's a mutation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct--that's why x is memoized (its mutable range doesn't extend beyond the hook call) but y is not memoized (custom.notPresent() may mutate it, so its range extends over the hook call)


return <Foo x={x} y={y} />;
}

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @customType

import custom from "custom";

function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.x) {
t0 = [props.x];
$[0] = props.x;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const y = [props.y];

useHook();

custom(x);
custom.prop(x);
custom.notPresent(y);
return <Foo x={x} y={y} />;
}

```

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @customType

import custom from 'custom';

function Component(props) {
const x = [props.x];
const y = [props.y];

useHook();

custom(x);
custom.prop(x);
custom.notPresent(y);

return <Foo x={x} y={y} />;
}
1 change: 1 addition & 0 deletions compiler/packages/snap/src/SproutTodoFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ const skipFilter = new Set([
// Depends on external functions
'idx-method-no-outlining-wildcard',
'idx-method-no-outlining',
'custom-type',

// needs to be executed as a module
'meta-property',
Expand Down
34 changes: 34 additions & 0 deletions compiler/packages/snap/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
} from 'babel-plugin-react-compiler/src/Entrypoint';
import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR';
import type {
GlobalType,
Macro,
MacroMethod,
parseConfigPragma as ParseConfigPragma,
Expand Down Expand Up @@ -52,6 +53,39 @@ function makePluginOptions(
let enableChangeDetectionForDebugging = null;
let customMacros: null | Array<Macro> = null;
let validateBlocklistedImports = null;
let typedGlobals: Array<[string, GlobalType]> = [];

if (firstLine.indexOf('@customType') !== -1) {
typedGlobals.push([
'custom',
{
id: null,
fn: {
positionalParams: [],
restParam: 'read' as Effect.Read,
calleeEffect: 'read' as Effect.Read,
returnType: null,
returnValueKind: 'primitive' as ValueKind.Primitive,
},
properties: [
[
'prop',
{
id: null,
fn: {
positionalParams: [],
restParam: 'read' as Effect.Read,
calleeEffect: 'read' as Effect.Read,
returnType: null,
returnValueKind: 'primitive' as ValueKind.Primitive,
},
properties: [],
},
],
],
},
]);
}

if (firstLine.indexOf('@compilationMode(annotation)') !== -1) {
assert(
Expand Down