diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts new file mode 100644 index 0000000000000..c63feb830feeb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts @@ -0,0 +1,752 @@ +/** + * TypeScript definitions for Flow type JSON representations + * Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml + */ + +// Base type for all Flow types with a kind field +export interface BaseFlowType { + kind: string; +} + +// Type for representing polarity +export type Polarity = 'positive' | 'negative' | 'neutral'; + +// Type for representing a name that might be null +export type OptionalName = string | null; + +// Open type +export interface OpenType extends BaseFlowType { + kind: 'Open'; +} + +// Def type +export interface DefType extends BaseFlowType { + kind: 'Def'; + def: DefT; +} + +// Eval type +export interface EvalType extends BaseFlowType { + kind: 'Eval'; + type: FlowType; + destructor: Destructor; +} + +// Generic type +export interface GenericType extends BaseFlowType { + kind: 'Generic'; + name: string; + bound: FlowType; + no_infer: boolean; +} + +// ThisInstance type +export interface ThisInstanceType extends BaseFlowType { + kind: 'ThisInstance'; + instance: InstanceT; + is_this: boolean; + name: string; +} + +// ThisTypeApp type +export interface ThisTypeAppType extends BaseFlowType { + kind: 'ThisTypeApp'; + t1: FlowType; + t2: FlowType; + t_list?: Array; +} + +// TypeApp type +export interface TypeAppType extends BaseFlowType { + kind: 'TypeApp'; + type: FlowType; + targs: Array; + from_value: boolean; + use_desc: boolean; +} + +// FunProto type +export interface FunProtoType extends BaseFlowType { + kind: 'FunProto'; +} + +// ObjProto type +export interface ObjProtoType extends BaseFlowType { + kind: 'ObjProto'; +} + +// NullProto type +export interface NullProtoType extends BaseFlowType { + kind: 'NullProto'; +} + +// FunProtoBind type +export interface FunProtoBindType extends BaseFlowType { + kind: 'FunProtoBind'; +} + +// Intersection type +export interface IntersectionType extends BaseFlowType { + kind: 'Intersection'; + members: Array; +} + +// Union type +export interface UnionType extends BaseFlowType { + kind: 'Union'; + members: Array; +} + +// Maybe type +export interface MaybeType extends BaseFlowType { + kind: 'Maybe'; + type: FlowType; +} + +// Optional type +export interface OptionalType extends BaseFlowType { + kind: 'Optional'; + type: FlowType; + use_desc: boolean; +} + +// Keys type +export interface KeysType extends BaseFlowType { + kind: 'Keys'; + type: FlowType; +} + +// Annot type +export interface AnnotType extends BaseFlowType { + kind: 'Annot'; + type: FlowType; + use_desc: boolean; +} + +// Opaque type +export interface OpaqueType extends BaseFlowType { + kind: 'Opaque'; + opaquetype: { + opaque_id: string; + underlying_t: FlowType | null; + super_t: FlowType | null; + opaque_type_args: Array<{ + name: string; + type: FlowType; + polarity: Polarity; + }>; + opaque_name: string; + }; +} + +// Namespace type +export interface NamespaceType extends BaseFlowType { + kind: 'Namespace'; + namespace_symbol: { + symbol: string; + }; + values_type: FlowType; + types_tmap: PropertyMap; +} + +// Any type +export interface AnyType extends BaseFlowType { + kind: 'Any'; +} + +// StrUtil type +export interface StrUtilType extends BaseFlowType { + kind: 'StrUtil'; + op: 'StrPrefix' | 'StrSuffix'; + prefix?: string; + suffix?: string; + remainder?: FlowType; +} + +// TypeParam definition +export interface TypeParam { + name: string; + bound: FlowType; + polarity: Polarity; + default: FlowType | null; +} + +// EnumInfo types +export type EnumInfo = ConcreteEnum | AbstractEnum; + +export interface ConcreteEnum { + kind: 'ConcreteEnum'; + enum_name: string; + enum_id: string; + members: Array; + representation_t: FlowType; + has_unknown_members: boolean; +} + +export interface AbstractEnum { + kind: 'AbstractEnum'; + representation_t: FlowType; +} + +// CanonicalRendersForm types +export type CanonicalRendersForm = + | InstrinsicRenders + | NominalRenders + | StructuralRenders + | DefaultRenders; + +export interface InstrinsicRenders { + kind: 'InstrinsicRenders'; + name: string; +} + +export interface NominalRenders { + kind: 'NominalRenders'; + renders_id: string; + renders_name: string; + renders_super: FlowType; +} + +export interface StructuralRenders { + kind: 'StructuralRenders'; + renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar'; + renders_structural_type: FlowType; +} + +export interface DefaultRenders { + kind: 'DefaultRenders'; +} + +// InstanceT definition +export interface InstanceT { + inst: InstType; + static: FlowType; + super: FlowType; + implements: Array; +} + +// InstType definition +export interface InstType { + class_name: string | null; + class_id: string; + type_args: Array<{ + name: string; + type: FlowType; + polarity: Polarity; + }>; + own_props: PropertyMap; + proto_props: PropertyMap; + call_t: null | { + id: number; + call: FlowType; + }; +} + +// DefT types +export type DefT = + | NumGeneralType + | StrGeneralType + | BoolGeneralType + | BigIntGeneralType + | EmptyType + | MixedType + | NullType + | VoidType + | SymbolType + | FunType + | ObjType + | ArrType + | ClassType + | InstanceType + | SingletonStrType + | NumericStrKeyType + | SingletonNumType + | SingletonBoolType + | SingletonBigIntType + | TypeType + | PolyType + | ReactAbstractComponentType + | RendersType + | EnumValueType + | EnumObjectType; + +export interface NumGeneralType extends BaseFlowType { + kind: 'NumGeneral'; +} + +export interface StrGeneralType extends BaseFlowType { + kind: 'StrGeneral'; +} + +export interface BoolGeneralType extends BaseFlowType { + kind: 'BoolGeneral'; +} + +export interface BigIntGeneralType extends BaseFlowType { + kind: 'BigIntGeneral'; +} + +export interface EmptyType extends BaseFlowType { + kind: 'Empty'; +} + +export interface MixedType extends BaseFlowType { + kind: 'Mixed'; +} + +export interface NullType extends BaseFlowType { + kind: 'Null'; +} + +export interface VoidType extends BaseFlowType { + kind: 'Void'; +} + +export interface SymbolType extends BaseFlowType { + kind: 'Symbol'; +} + +export interface FunType extends BaseFlowType { + kind: 'Fun'; + static: FlowType; + funtype: FunTypeObj; +} + +export interface ObjType extends BaseFlowType { + kind: 'Obj'; + objtype: ObjTypeObj; +} + +export interface ArrType extends BaseFlowType { + kind: 'Arr'; + arrtype: ArrTypeObj; +} + +export interface ClassType extends BaseFlowType { + kind: 'Class'; + type: FlowType; +} + +export interface InstanceType extends BaseFlowType { + kind: 'Instance'; + instance: InstanceT; +} + +export interface SingletonStrType extends BaseFlowType { + kind: 'SingletonStr'; + from_annot: boolean; + value: string; +} + +export interface NumericStrKeyType extends BaseFlowType { + kind: 'NumericStrKey'; + number: string; + string: string; +} + +export interface SingletonNumType extends BaseFlowType { + kind: 'SingletonNum'; + from_annot: boolean; + number: string; + string: string; +} + +export interface SingletonBoolType extends BaseFlowType { + kind: 'SingletonBool'; + from_annot: boolean; + value: boolean; +} + +export interface SingletonBigIntType extends BaseFlowType { + kind: 'SingletonBigInt'; + from_annot: boolean; + value: string; +} + +export interface TypeType extends BaseFlowType { + kind: 'Type'; + type_kind: TypeTKind; + type: FlowType; +} + +export type TypeTKind = + | 'TypeAliasKind' + | 'TypeParamKind' + | 'OpaqueKind' + | 'ImportTypeofKind' + | 'ImportClassKind' + | 'ImportEnumKind' + | 'InstanceKind' + | 'RenderTypeKind'; + +export interface PolyType extends BaseFlowType { + kind: 'Poly'; + tparams: Array; + t_out: FlowType; + id: string; +} + +export interface ReactAbstractComponentType extends BaseFlowType { + kind: 'ReactAbstractComponent'; + config: FlowType; + renders: FlowType; + instance: ComponentInstance; + component_kind: ComponentKind; +} + +export type ComponentInstance = + | {kind: 'RefSetterProp'; type: FlowType} + | {kind: 'Omitted'}; + +export type ComponentKind = + | {kind: 'Structural'} + | {kind: 'Nominal'; id: string; name: string; types: Array | null}; + +export interface RendersType extends BaseFlowType { + kind: 'Renders'; + form: CanonicalRendersForm; +} + +export interface EnumValueType extends BaseFlowType { + kind: 'EnumValue'; + enum_info: EnumInfo; +} + +export interface EnumObjectType extends BaseFlowType { + kind: 'EnumObject'; + enum_value_t: FlowType; + enum_info: EnumInfo; +} + +// ObjKind types +export type ObjKind = + | {kind: 'Exact'} + | {kind: 'Inexact'} + | {kind: 'Indexed'; dicttype: DictType}; + +// DictType definition +export interface DictType { + dict_name: string | null; + key: FlowType; + value: FlowType; + dict_polarity: Polarity; +} + +// ArrType types +export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT; + +export interface ArrayAT { + kind: 'ArrayAT'; + elem_t: FlowType; +} + +export interface TupleAT { + kind: 'TupleAT'; + elem_t: FlowType; + elements: Array; + min_arity: number; + max_arity: number; + inexact: boolean; +} + +export interface ROArrayAT { + kind: 'ROArrayAT'; + elem_t: FlowType; +} + +// TupleElement definition +export interface TupleElement { + name: string | null; + t: FlowType; + polarity: Polarity; + optional: boolean; +} + +// Flags definition +export interface Flags { + obj_kind: ObjKind; +} + +// Property types +export type Property = + | FieldProperty + | GetProperty + | SetProperty + | GetSetProperty + | MethodProperty; + +export interface FieldProperty { + kind: 'Field'; + type: FlowType; + polarity: Polarity; +} + +export interface GetProperty { + kind: 'Get'; + type: FlowType; +} + +export interface SetProperty { + kind: 'Set'; + type: FlowType; +} + +export interface GetSetProperty { + kind: 'GetSet'; + get_type: FlowType; + set_type: FlowType; +} + +export interface MethodProperty { + kind: 'Method'; + type: FlowType; +} + +// PropertyMap definition +export interface PropertyMap { + [key: string]: Property; // For other properties in the map +} + +// ObjType definition +export interface ObjTypeObj { + flags: Flags; + props: PropertyMap; + proto_t: FlowType; + call_t: number | null; +} + +// FunType definition +export interface FunTypeObj { + this_t: { + type: FlowType; + status: ThisStatus; + }; + params: Array<{ + name: string | null; + type: FlowType; + }>; + rest_param: null | { + name: string | null; + type: FlowType; + }; + return_t: FlowType; + type_guard: null | { + inferred: boolean; + param_name: string; + type_guard: FlowType; + one_sided: boolean; + }; + effect: Effect; +} + +// ThisStatus types +export type ThisStatus = + | {kind: 'This_Method'; unbound: boolean} + | {kind: 'This_Function'}; + +// Effect types +export type Effect = + | {kind: 'HookDecl'; id: string} + | {kind: 'HookAnnot'} + | {kind: 'ArbitraryEffect'} + | {kind: 'AnyEffect'}; + +// Destructor types +export type Destructor = + | NonMaybeTypeDestructor + | PropertyTypeDestructor + | ElementTypeDestructor + | OptionalIndexedAccessNonMaybeTypeDestructor + | OptionalIndexedAccessResultTypeDestructor + | ExactTypeDestructor + | ReadOnlyTypeDestructor + | PartialTypeDestructor + | RequiredTypeDestructor + | SpreadTypeDestructor + | SpreadTupleTypeDestructor + | RestTypeDestructor + | ValuesTypeDestructor + | ConditionalTypeDestructor + | TypeMapDestructor + | ReactElementPropsTypeDestructor + | ReactElementConfigTypeDestructor + | ReactCheckComponentConfigDestructor + | ReactDRODestructor + | MakeHooklikeDestructor + | MappedTypeDestructor + | EnumTypeDestructor; + +export interface NonMaybeTypeDestructor { + kind: 'NonMaybeType'; +} + +export interface PropertyTypeDestructor { + kind: 'PropertyType'; + name: string; +} + +export interface ElementTypeDestructor { + kind: 'ElementType'; + index_type: FlowType; +} + +export interface OptionalIndexedAccessNonMaybeTypeDestructor { + kind: 'OptionalIndexedAccessNonMaybeType'; + index: OptionalIndexedAccessIndex; +} + +export type OptionalIndexedAccessIndex = + | {kind: 'StrLitIndex'; name: string} + | {kind: 'TypeIndex'; type: FlowType}; + +export interface OptionalIndexedAccessResultTypeDestructor { + kind: 'OptionalIndexedAccessResultType'; +} + +export interface ExactTypeDestructor { + kind: 'ExactType'; +} + +export interface ReadOnlyTypeDestructor { + kind: 'ReadOnlyType'; +} + +export interface PartialTypeDestructor { + kind: 'PartialType'; +} + +export interface RequiredTypeDestructor { + kind: 'RequiredType'; +} + +export interface SpreadTypeDestructor { + kind: 'SpreadType'; + target: SpreadTarget; + operands: Array; + operand_slice: Slice | null; +} + +export type SpreadTarget = + | {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'} + | {kind: 'Annot'; make_exact: boolean}; + +export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice; + +export interface Slice { + kind: 'Slice'; + prop_map: PropertyMap; + generics: Array; + dict: DictType | null; + reachable_targs: Array<{ + type: FlowType; + polarity: Polarity; + }>; +} + +export interface SpreadTupleTypeDestructor { + kind: 'SpreadTupleType'; + inexact: boolean; + resolved_rev: string; + unresolved: string; +} + +export interface RestTypeDestructor { + kind: 'RestType'; + merge_mode: RestMergeMode; + type: FlowType; +} + +export type RestMergeMode = + | {kind: 'SpreadReversal'} + | {kind: 'ReactConfigMerge'; polarity: Polarity} + | {kind: 'Omit'}; + +export interface ValuesTypeDestructor { + kind: 'ValuesType'; +} + +export interface ConditionalTypeDestructor { + kind: 'ConditionalType'; + distributive_tparam_name: string | null; + infer_tparams: string; + extends_t: FlowType; + true_t: FlowType; + false_t: FlowType; +} + +export interface TypeMapDestructor { + kind: 'ObjectKeyMirror'; +} + +export interface ReactElementPropsTypeDestructor { + kind: 'ReactElementPropsType'; +} + +export interface ReactElementConfigTypeDestructor { + kind: 'ReactElementConfigType'; +} + +export interface ReactCheckComponentConfigDestructor { + kind: 'ReactCheckComponentConfig'; + props: { + [key: string]: Property; + }; +} + +export interface ReactDRODestructor { + kind: 'ReactDRO'; + dro_type: + | 'HookReturn' + | 'HookArg' + | 'Props' + | 'ImmutableAnnot' + | 'DebugAnnot'; +} + +export interface MakeHooklikeDestructor { + kind: 'MakeHooklike'; +} + +export interface MappedTypeDestructor { + kind: 'MappedType'; + homomorphic: Homomorphic; + distributive_tparam_name: string | null; + property_type: FlowType; + mapped_type_flags: { + variance: Polarity; + optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality'; + }; +} + +export type Homomorphic = + | {kind: 'Homomorphic'} + | {kind: 'Unspecialized'} + | {kind: 'SemiHomomorphic'; type: FlowType}; + +export interface EnumTypeDestructor { + kind: 'EnumType'; +} + +// Union of all possible Flow types +export type FlowType = + | OpenType + | DefType + | EvalType + | GenericType + | ThisInstanceType + | ThisTypeAppType + | TypeAppType + | FunProtoType + | ObjProtoType + | NullProtoType + | FunProtoBindType + | IntersectionType + | UnionType + | MaybeType + | OptionalType + | KeysType + | AnnotType + | OpaqueType + | NamespaceType + | AnyType + | StrUtilType; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts new file mode 100644 index 0000000000000..fa3f551ff5fc3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts @@ -0,0 +1,131 @@ +import {CompilerError, SourceLocation} from '..'; +import { + ConcreteType, + printConcrete, + printType, + StructuralValue, + Type, + VariableId, +} from './Types'; + +export function unsupportedLanguageFeature( + desc: string, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Typedchecker does not currently support language feature: ${desc}`, + loc, + }); +} + +export type UnificationError = + | { + kind: 'TypeUnification'; + left: ConcreteType; + right: ConcreteType; + } + | { + kind: 'StructuralUnification'; + left: StructuralValue; + right: ConcreteType; + }; + +function printUnificationError(err: UnificationError): string { + if (err.kind === 'TypeUnification') { + return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`; + } else { + return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`; + } +} + +export function raiseUnificationErrors( + errs: null | Array, + loc: SourceLocation, +): void { + if (errs != null) { + if (errs.length === 0) { + CompilerError.invariant(false, { + reason: 'Should not have array of zero errors', + loc, + }); + } else if (errs.length === 1) { + CompilerError.throwInvalidJS({ + reason: `Unable to unify types because ${printUnificationError(errs[0])}`, + loc, + }); + } else { + const messages = errs + .map(err => `\t* ${printUnificationError(err)}`) + .join('\n'); + CompilerError.throwInvalidJS({ + reason: `Unable to unify types because:\n${messages}`, + loc, + }); + } + } +} + +export function unresolvableTypeVariable( + id: VariableId, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Unable to resolve free variable ${id} to a concrete type`, + loc, + }); +} + +export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never { + if (explicit) { + CompilerError.throwInvalidJS({ + reason: `Undefined is not a valid operand of \`+\``, + loc, + }); + } else { + CompilerError.throwInvalidJS({ + reason: `Value may be undefined, which is not a valid operand of \`+\``, + loc, + }); + } +} + +export function unsupportedTypeAnnotation( + desc: string, + loc: SourceLocation, +): never { + CompilerError.throwInvalidJS({ + reason: `Typedchecker does not currently support type annotation: ${desc}`, + loc, + }); +} + +export function checkTypeArgumentArity( + desc: string, + expected: number, + actual: number, + loc: SourceLocation, +): void { + if (expected !== actual) { + CompilerError.throwInvalidJS({ + reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`, + loc, + }); + } +} + +export function notAFunction(desc: string, loc: SourceLocation): void { + CompilerError.throwInvalidJS({ + reason: `Cannot call ${desc} because it is not a function`, + loc, + }); +} + +export function notAPolymorphicFunction( + desc: string, + loc: SourceLocation, +): void { + CompilerError.throwInvalidJS({ + reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`, + loc, + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts new file mode 100644 index 0000000000000..0a514f090d2a2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts @@ -0,0 +1,312 @@ +import {GeneratedSource} from '../HIR'; +import {assertExhaustive} from '../Utils/utils'; +import {unsupportedLanguageFeature} from './TypeErrors'; +import { + ConcreteType, + ResolvedType, + TypeParameter, + TypeParameterId, + DEBUG, + printConcrete, + printType, +} from './Types'; + +export function substitute( + type: ConcreteType, + typeParameters: Array>, + typeArguments: Array, +): ResolvedType { + const substMap = new Map(); + for (let i = 0; i < typeParameters.length; i++) { + // TODO: Length checks to make sure type params match up with args + const typeParameter = typeParameters[i]; + const typeArgument = typeArguments[i]; + substMap.set(typeParameter.id, typeArgument); + } + const substitutionFunction = (t: ResolvedType): ResolvedType => { + // TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types + if (t.type.kind === 'Generic' && substMap.has(t.type.id)) { + const substitutedType = substMap.get(t.type.id)!; + return substitutedType; + } + + return { + kind: 'Concrete', + type: mapType(substitutionFunction, t.type), + platform: t.platform, + }; + }; + + const substituted = mapType(substitutionFunction, type); + + if (DEBUG) { + let substs = ''; + for (let i = 0; i < typeParameters.length; i++) { + const typeParameter = typeParameters[i]; + const typeArgument = typeArguments[i]; + substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`; + } + console.log( + `${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`, + ); + } + + return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'}; +} + +export function mapType( + f: (t: T) => U, + type: ConcreteType, +): ConcreteType { + switch (type.kind) { + case 'Mixed': + case 'Number': + case 'String': + case 'Boolean': + case 'Void': + return type; + + case 'Nullable': + return { + kind: 'Nullable', + type: f(type.type), + }; + + case 'Array': + return { + kind: 'Array', + element: f(type.element), + }; + + case 'Set': + return { + kind: 'Set', + element: f(type.element), + }; + + case 'Map': + return { + kind: 'Map', + key: f(type.key), + value: f(type.value), + }; + + case 'Function': + return { + kind: 'Function', + typeParameters: + type.typeParameters?.map(param => ({ + id: param.id, + name: param.name, + bound: f(param.bound), + })) ?? null, + params: type.params.map(f), + returnType: f(type.returnType), + }; + + case 'Component': { + return { + kind: 'Component', + children: type.children != null ? f(type.children) : null, + props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])), + }; + } + + case 'Generic': + return { + kind: 'Generic', + id: type.id, + bound: f(type.bound), + }; + + case 'Object': + return type; + + case 'Tuple': + return { + kind: 'Tuple', + id: type.id, + members: type.members.map(f), + }; + + case 'Structural': + return type; + + case 'Enum': + case 'Union': + case 'Instance': + unsupportedLanguageFeature(type.kind, GeneratedSource); + + default: + assertExhaustive(type, 'Unknown type kind'); + } +} + +export function diff( + a: ConcreteType, + b: ConcreteType, + onChild: (a: T, b: T) => R, + onChildMismatch: (child: R, cur: R) => R, + onMismatch: (a: ConcreteType, b: ConcreteType, cur: R) => R, + init: R, +): R { + let errors = init; + + // Check if kinds match + if (a.kind !== b.kind) { + errors = onMismatch(a, b, errors); + return errors; + } + + // Based on kind, check other properties + switch (a.kind) { + case 'Mixed': + case 'Number': + case 'String': + case 'Boolean': + case 'Void': + // Simple types, no further checks needed + break; + + case 'Nullable': + // Check the nested type + errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors); + break; + + case 'Array': + case 'Set': + // Check the element type + errors = onChildMismatch( + onChild(a.element, (b as typeof a).element), + errors, + ); + break; + + case 'Map': + // Check both key and value types + errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors); + errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors); + break; + + case 'Function': { + const bFunc = b as typeof a; + + // Check type parameters + if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) { + errors = onMismatch(a, b, errors); + } + + if (a.typeParameters != null && bFunc.typeParameters != null) { + if (a.typeParameters.length !== bFunc.typeParameters.length) { + errors = onMismatch(a, b, errors); + } + + // Type parameters are just numbers, so we can compare them directly + for (let i = 0; i < a.typeParameters.length; i++) { + if (a.typeParameters[i] !== bFunc.typeParameters[i]) { + errors = onMismatch(a, b, errors); + } + } + } + + // Check parameters + if (a.params.length !== bFunc.params.length) { + errors = onMismatch(a, b, errors); + } + + for (let i = 0; i < a.params.length; i++) { + errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors); + } + + // Check return type + errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors); + break; + } + + case 'Component': { + const bComp = b as typeof a; + + // Check children + if (a.children !== bComp.children) { + errors = onMismatch(a, b, errors); + } + + // Check props + if (a.props.size !== bComp.props.size) { + errors = onMismatch(a, b, errors); + } + + for (const [k, v] of a.props) { + const bProp = bComp.props.get(k); + if (bProp == null) { + errors = onMismatch(a, b, errors); + } else { + errors = onChildMismatch(onChild(v, bProp), errors); + } + } + + break; + } + + case 'Generic': { + // Check that the type parameter IDs match + if (a.id !== (b as typeof a).id) { + errors = onMismatch(a, b, errors); + } + break; + } + case 'Structural': { + const bStruct = b as typeof a; + + // Check that the structural IDs match + if (a.id !== bStruct.id) { + errors = onMismatch(a, b, errors); + } + break; + } + case 'Object': { + const bNom = b as typeof a; + + // Check that the nominal IDs match + if (a.id !== bNom.id) { + errors = onMismatch(a, b, errors); + } + break; + } + + case 'Tuple': { + const bTuple = b as typeof a; + + // Check that the tuple IDs match + if (a.id !== bTuple.id) { + errors = onMismatch(a, b, errors); + } + for (let i = 0; i < a.members.length; i++) { + errors = onChildMismatch( + onChild(a.members[i], bTuple.members[i]), + errors, + ); + } + + break; + } + + case 'Enum': + case 'Instance': + case 'Union': { + unsupportedLanguageFeature(a.kind, GeneratedSource); + } + + default: + assertExhaustive(a, 'Unknown type kind'); + } + + return errors; +} + +export function filterOptional(t: ResolvedType): ResolvedType { + if (t.kind === 'Concrete' && t.type.kind === 'Nullable') { + return t.type.type; + } + return t; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts b/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts new file mode 100644 index 0000000000000..b4f91ab478e69 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts @@ -0,0 +1,1001 @@ +import {CompilerError, SourceLocation} from '..'; +import { + Environment, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, +} from '../HIR'; +import * as t from '@babel/types'; +import * as TypeErrors from './TypeErrors'; +import {assertExhaustive} from '../Utils/utils'; + +export const DEBUG = false; + +export type Type = + | {kind: 'Concrete'; type: ConcreteType; platform: Platform} + | {kind: 'Variable'; id: VariableId}; + +export type ResolvedType = { + kind: 'Concrete'; + type: ConcreteType; + platform: Platform; +}; + +export type ComponentType = { + kind: 'Component'; + props: Map; + children: null | T; +}; +export type ConcreteType = + | {kind: 'Enum'} + | {kind: 'Mixed'} + | {kind: 'Number'} + | {kind: 'String'} + | {kind: 'Boolean'} + | {kind: 'Void'} + | {kind: 'Nullable'; type: T} + | {kind: 'Array'; element: T} + | {kind: 'Set'; element: T} + | {kind: 'Map'; key: T; value: T} + | { + kind: 'Function'; + typeParameters: null | Array>; + params: Array; + returnType: T; + } + | ComponentType + | {kind: 'Generic'; id: TypeParameterId; bound: T} + | { + kind: 'Object'; + id: NominalId; + members: Map; + } + | { + kind: 'Tuple'; + id: NominalId; + members: Array; + } + | {kind: 'Structural'; id: LinearId} + | {kind: 'Union'; members: Array} + | {kind: 'Instance'; name: string; members: Map}; + +export type StructuralValue = + | { + kind: 'Function'; + fn: HIRFunction; + } + | { + kind: 'Object'; + members: Map; + } + | { + kind: 'Array'; + elementType: ResolvedType; + }; + +export type Structural = { + type: StructuralValue; + consumed: boolean; +}; +// TODO: create a kind: "Alias" + +// type T = { foo: X} + +/** + * + * function apply(x: A, f: A => B): B { } + * + * apply(42, x => String(x)); + * + * f({foo: 42}) + * + * f([HOLE]) -----> {foo: 42} with context NominalType + * + * $0 = Object {foo: 42} + * $1 = LoadLocal "f" + * $2 = Call $1, [$0] + * + * ContextMap: + * $2 => ?? + * $1 => [HOLE]($0) + * $0 => $1([HOLE]) + */ + +/* + *const g = {foo: 42} as NominalType // ok + * + * + *function f(x: NominalType) { ... } + *f() + * + *const y: NominalType = {foo: 42} + * + * + */ + +/** + * // Mike: maybe this could be the ideal? + *type X = nominal('registryNameX', { + *value: number, + *}); + * + * // For now: + *opaque type X = { // creates a new nominal type + *value: number, + *}; + * + *type Y = X; // creates a type alias + * + *type Z = number; // creates a type alias + * + * + * // (todo: disallowed) + *type X' = { + *value: number, + *} + */ + +export type TypeParameter = { + name: string; + id: TypeParameterId; + bound: T; +}; + +const opaqueLinearId = Symbol(); +export type LinearId = number & { + [opaqueLinearId]: 'LinearId'; +}; + +export function makeLinearId(id: number): LinearId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected LinearId id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as LinearId; +} + +const opaqueTypeParameterId = Symbol(); +export type TypeParameterId = number & { + [opaqueTypeParameterId]: 'TypeParameterId'; +}; + +export function makeTypeParameterId(id: number): TypeParameterId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected TypeParameterId to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as TypeParameterId; +} + +const opaqueNominalId = Symbol(); +export type NominalId = number & { + [opaqueNominalId]: 'NominalId'; +}; + +export function makeNominalId(id: number): NominalId { + return id as NominalId; +} + +const opaqueVariableId = Symbol(); +export type VariableId = number & { + [opaqueVariableId]: 'VariableId'; +}; + +export function makeVariableId(id: number): VariableId { + CompilerError.invariant(id >= 0 && Number.isInteger(id), { + reason: 'Expected VariableId id to be a non-negative integer', + description: null, + loc: null, + suggestions: null, + }); + return id as VariableId; +} + +import {inspect} from 'util'; +import {FlowType} from './FlowTypes'; +export function printConcrete( + type: ConcreteType, + printType: (_: T) => string, +): string { + switch (type.kind) { + case 'Mixed': + return 'mixed'; + case 'Number': + return 'number'; + case 'String': + return 'string'; + case 'Boolean': + return 'boolean'; + case 'Void': + return 'void'; + case 'Nullable': + return `${printType(type.type)} | void`; + case 'Array': + return `Array<${printType(type.element)}>`; + case 'Set': + return `Set<${printType(type.element)}>`; + case 'Map': + return `Map<${printType(type.key)}, ${printType(type.value)}>`; + case 'Function': { + const typeParams = type.typeParameters + ? `<${type.typeParameters.map(tp => `T${tp}`).join(', ')}>` + : ''; + const params = type.params.map(printType).join(', '); + const returnType = printType(type.returnType); + return `${typeParams}(${params}) => ${returnType}`; + } + case 'Component': { + const params = [...type.props.entries()] + .map(([k, v]) => `${k}: ${printType(v)}`) + .join(', '); + const comma = type.children != null && type.props.size > 0 ? ', ' : ''; + const children = + type.children != null ? `children: ${printType(type.children)}` : ''; + return `component (${params}${comma}${children})`; + } + case 'Generic': + return `T${type.id}`; + case 'Object': { + const name = `Object ${inspect([...type.members.keys()])}`; + return `${name}`; + } + case 'Tuple': { + const name = `Tuple ${type.members}`; + return `${name}`; + } + case 'Structural': { + const name = `Structural ${type.id}`; + return `${name}`; + } + case 'Enum': { + return 'TODO enum printing'; + } + case 'Union': { + return type.members.map(printType).join(' | '); + } + case 'Instance': { + return type.name; + } + default: + assertExhaustive(type, `Unknown type: ${JSON.stringify(type)}`); + } +} + +export function printType(type: Type): string { + switch (type.kind) { + case 'Concrete': + return printConcrete(type.type, printType); + case 'Variable': + return `$${type.id}`; + default: + assertExhaustive(type, `Unknown type: ${JSON.stringify(type)}`); + } +} + +export function printResolved(type: ResolvedType): string { + return printConcrete(type.type, printResolved); +} + +type Platform = 'client' | 'server' | 'shared'; + +const DUMMY_NOMINAL = makeNominalId(0); + +function convertFlowType(flowType: FlowType, loc: string): ResolvedType { + let nextGenericId = 0; + function convertFlowTypeImpl( + flowType: FlowType, + loc: string, + genericEnv: Map, + platform: Platform, + poly: null | Array> = null, + ): ResolvedType { + switch (flowType.kind) { + case 'TypeApp': { + if ( + flowType.type.kind === 'Def' && + flowType.type.def.kind === 'Poly' && + flowType.type.def.t_out.kind === 'Def' && + flowType.type.def.t_out.def.kind === 'Type' && + flowType.type.def.t_out.def.type.kind === 'Opaque' && + flowType.type.def.t_out.def.type.opaquetype.opaque_name === + 'Client' && + flowType.targs.length === 1 + ) { + return convertFlowTypeImpl( + flowType.targs[0], + loc, + genericEnv, + 'client', + ); + } else if ( + flowType.type.kind === 'Def' && + flowType.type.def.kind === 'Poly' && + flowType.type.def.t_out.kind === 'Def' && + flowType.type.def.t_out.def.kind === 'Type' && + flowType.type.def.t_out.def.type.kind === 'Opaque' && + flowType.type.def.t_out.def.type.opaquetype.opaque_name === + 'Server' && + flowType.targs.length === 1 + ) { + return convertFlowTypeImpl( + flowType.targs[0], + loc, + genericEnv, + 'server', + ); + } + return Resolved.todo(platform); + } + case 'Open': + return Resolved.mixed(platform); + case 'Any': + return Resolved.todo(platform); + case 'Annot': + return convertFlowTypeImpl( + flowType.type, + loc, + genericEnv, + platform, + poly, + ); + case 'Opaque': { + if ( + flowType.opaquetype.opaque_name === 'Client' && + flowType.opaquetype.super_t != null + ) { + return convertFlowTypeImpl( + flowType.opaquetype.super_t, + loc, + genericEnv, + 'client', + ); + } + if ( + flowType.opaquetype.opaque_name === 'Server' && + flowType.opaquetype.super_t != null + ) { + return convertFlowTypeImpl( + flowType.opaquetype.super_t, + loc, + genericEnv, + 'server', + ); + } + const t = + flowType.opaquetype.underlying_t ?? flowType.opaquetype.super_t; + if (t != null) { + return convertFlowTypeImpl(t, loc, genericEnv, platform, poly); + } else { + return Resolved.todo(platform); + } + } + case 'Def': { + switch (flowType.def.kind) { + case 'EnumValue': + return convertFlowTypeImpl( + flowType.def.enum_info.representation_t, + loc, + genericEnv, + platform, + poly, + ); + case 'EnumObject': + return Resolved.enum(platform); + case 'Empty': + return Resolved.todo(platform); + case 'Instance': { + const members = new Map(); + for (const key in flowType.def.instance.inst.own_props) { + const prop = flowType.def.instance.inst.own_props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.class( + flowType.def.instance.inst.class_name ?? '[anonymous class]', + members, + platform, + ); + } + case 'Type': + return convertFlowTypeImpl( + flowType.def.type, + loc, + genericEnv, + platform, + poly, + ); + case 'NumGeneral': + case 'SingletonNum': + return Resolved.number(platform); + case 'StrGeneral': + case 'SingletonStr': + return Resolved.string(platform); + case 'BoolGeneral': + case 'SingletonBool': + return Resolved.boolean(platform); + case 'Void': + return Resolved.void(platform); + case 'Null': + return Resolved.void(platform); + case 'Mixed': + return Resolved.mixed(platform); + case 'Arr': { + if ( + flowType.def.arrtype.kind === 'ArrayAT' || + flowType.def.arrtype.kind === 'ROArrayAT' + ) { + return Resolved.array( + convertFlowTypeImpl( + flowType.def.arrtype.elem_t, + loc, + genericEnv, + platform, + ), + platform, + ); + } else { + return Resolved.tuple( + DUMMY_NOMINAL, + flowType.def.arrtype.elements.map(t => + convertFlowTypeImpl(t.t, loc, genericEnv, platform), + ), + platform, + ); + } + } + case 'Obj': { + const members = new Map(); + for (const key in flowType.def.objtype.props) { + const prop = flowType.def.objtype.props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.object(DUMMY_NOMINAL, members, platform); + } + case 'Class': { + if (flowType.def.type.kind === 'ThisInstance') { + const members = new Map(); + for (const key in flowType.def.type.instance.inst.own_props) { + const prop = flowType.def.type.instance.inst.own_props[key]; + if (prop.kind === 'Field') { + members.set( + key, + convertFlowTypeImpl(prop.type, loc, genericEnv, platform), + ); + } else { + CompilerError.invariant(false, { + reason: `Unsupported property kind ${prop.kind}`, + loc: GeneratedSource, + }); + } + } + return Resolved.class( + flowType.def.type.instance.inst.class_name ?? + '[anonymous class]', + members, + platform, + ); + } + CompilerError.invariant(false, { + reason: `Unsupported class instance type ${flowType.def.type.kind}`, + loc: GeneratedSource, + }); + } + case 'Fun': + return Resolved.function( + poly, + flowType.def.funtype.params.map(p => + convertFlowTypeImpl(p.type, loc, genericEnv, platform), + ), + convertFlowTypeImpl( + flowType.def.funtype.return_t, + loc, + genericEnv, + platform, + ), + platform, + ); + case 'Poly': { + let newEnv = genericEnv; + const poly = flowType.def.tparams.map(p => { + const id = makeTypeParameterId(nextGenericId++); + const bound = convertFlowTypeImpl(p.bound, loc, newEnv, platform); + newEnv = new Map(newEnv); + newEnv.set(p.name, id); + return { + name: p.name, + id, + bound, + }; + }); + return convertFlowTypeImpl( + flowType.def.t_out, + loc, + newEnv, + platform, + poly, + ); + } + case 'ReactAbstractComponent': { + const props = new Map(); + let children: ResolvedType | null = null; + const propsType = convertFlowTypeImpl( + flowType.def.config, + loc, + genericEnv, + platform, + ); + + if (propsType.type.kind === 'Object') { + propsType.type.members.forEach((v, k) => { + if (k === 'children') { + children = v; + } else { + props.set(k, v); + } + }); + } else { + CompilerError.invariant(false, { + reason: `Unsupported component props type ${propsType.type.kind}`, + loc: GeneratedSource, + }); + } + + return Resolved.component(props, children, platform); + } + case 'Renders': + return Resolved.todo(platform); + default: + TypeErrors.unsupportedTypeAnnotation('Renders', GeneratedSource); + } + } + case 'Generic': { + const id = genericEnv.get(flowType.name); + if (id == null) { + TypeErrors.unsupportedTypeAnnotation(flowType.name, GeneratedSource); + } + return Resolved.generic( + id, + platform, + convertFlowTypeImpl(flowType.bound, loc, genericEnv, platform), + ); + } + case 'Union': { + const members = flowType.members.map(t => + convertFlowTypeImpl(t, loc, genericEnv, platform), + ); + if (members.length === 1) { + return members[0]; + } + if ( + members[0].type.kind === 'Number' || + members[0].type.kind === 'String' || + members[0].type.kind === 'Boolean' + ) { + const dupes = members.filter( + t => t.type.kind === members[0].type.kind, + ); + if (dupes.length === members.length) { + return members[0]; + } + } + if ( + members[0].type.kind === 'Array' && + (members[0].type.element.type.kind === 'Number' || + members[0].type.element.type.kind === 'String' || + members[0].type.element.type.kind === 'Boolean') + ) { + const first = members[0].type.element; + const dupes = members.filter( + t => + t.type.kind === 'Array' && + t.type.element.type.kind === first.type.kind, + ); + if (dupes.length === members.length) { + return members[0]; + } + } + return Resolved.union(members, platform); + } + case 'Eval': { + if ( + flowType.destructor.kind === 'ReactDRO' || + flowType.destructor.kind === 'ReactCheckComponentConfig' + ) { + return convertFlowTypeImpl( + flowType.type, + loc, + genericEnv, + platform, + poly, + ); + } + TypeErrors.unsupportedTypeAnnotation( + `EvalT(${flowType.destructor.kind})`, + GeneratedSource, + ); + } + case 'Optional': { + return Resolved.union( + [ + convertFlowTypeImpl(flowType.type, loc, genericEnv, platform), + Resolved.void(platform), + ], + platform, + ); + } + default: + TypeErrors.unsupportedTypeAnnotation(flowType.kind, GeneratedSource); + } + } + return convertFlowTypeImpl(flowType, loc, new Map(), 'shared'); +} + +export interface ITypeEnv { + popGeneric(name: string): void; + getGeneric(name: string): null | TypeParameter; + pushGeneric( + name: string, + binding: {name: string; id: TypeParameterId; bound: ResolvedType}, + ): void; + getType(id: Identifier): ResolvedType; + getTypeOrNull(id: Identifier): ResolvedType | null; + setType(id: Identifier, type: ResolvedType): void; + nextNominalId(): NominalId; + nextTypeParameterId(): TypeParameterId; + moduleEnv: Map; + addBinding(bindingIdentifier: t.Identifier, type: ResolvedType): void; + resolveBinding(bindingIdentifier: t.Identifier): ResolvedType | null; +} + +function serializeLoc(location: t.SourceLocation): string { + return `${location.start.line}:${location.start.column}-${location.end.line}:${location.end.column}`; +} + +function buildTypeEnvironment( + flowOutput: Array<{loc: t.SourceLocation; type: string}>, +): Map { + const result: Map = new Map(); + for (const item of flowOutput) { + const loc: t.SourceLocation = { + start: { + line: item.loc.start.line, + column: item.loc.start.column - 1, + index: item.loc.start.index, + }, + end: item.loc.end, + filename: item.loc.filename, + identifierName: item.loc.identifierName, + }; + + result.set(serializeLoc(loc), item.type); + } + return result; +} + +let lastFlowSource: string | null = null; +let lastFlowResult: any = null; + +export class FlowTypeEnv implements ITypeEnv { + moduleEnv: Map = new Map(); + #nextNominalId: number = 0; + #nextTypeParameterId: number = 0; + + #types: Map = new Map(); + #bindings: Map = new Map(); + #generics: Array<[string, TypeParameter]> = []; + #flowTypes: Map = new Map(); + + init(env: Environment, source: string): void { + // TODO: use flow-js only for web environments (e.g. playground) + CompilerError.invariant(env.config.flowTypeProvider != null, { + reason: 'Expected flowDumpTypes to be defined in environment config', + loc: GeneratedSource, + }); + let stdout: any; + if (source === lastFlowSource) { + stdout = lastFlowResult; + } else { + lastFlowSource = source; + lastFlowResult = env.config.flowTypeProvider(source); + stdout = lastFlowResult; + } + const flowTypes = buildTypeEnvironment(stdout); + const resolvedFlowTypes = new Map(); + for (const [loc, type] of flowTypes) { + if (typeof loc === 'symbol') continue; + resolvedFlowTypes.set(loc, convertFlowType(JSON.parse(type), loc)); + } + // =console.log(resolvedFlowTypes); + this.#flowTypes = resolvedFlowTypes; + } + + setType(identifier: Identifier, type: ResolvedType): void { + if ( + typeof identifier.loc !== 'symbol' && + this.#flowTypes.has(serializeLoc(identifier.loc)) + ) { + return; + } + this.#types.set(identifier.id, type); + } + + getType(identifier: Identifier): ResolvedType { + const result = this.getTypeOrNull(identifier); + if (result == null) { + throw new Error( + `Type not found for ${identifier.id}, ${typeof identifier.loc === 'symbol' ? 'generated loc' : serializeLoc(identifier.loc)}`, + ); + } + return result; + } + + getTypeOrNull(identifier: Identifier): ResolvedType | null { + const result = this.#types.get(identifier.id) ?? null; + if (result == null && typeof identifier.loc !== 'symbol') { + const flowType = this.#flowTypes.get(serializeLoc(identifier.loc)); + return flowType ?? null; + } + return result; + } + + getTypeByLoc(loc: SourceLocation): ResolvedType | null { + if (typeof loc === 'symbol') { + return null; + } + const flowType = this.#flowTypes.get(serializeLoc(loc)); + return flowType ?? null; + } + + nextNominalId(): NominalId { + return makeNominalId(this.#nextNominalId++); + } + + nextTypeParameterId(): TypeParameterId { + return makeTypeParameterId(this.#nextTypeParameterId++); + } + + addBinding(bindingIdentifier: t.Identifier, type: ResolvedType): void { + this.#bindings.set(bindingIdentifier, type); + } + + resolveBinding(bindingIdentifier: t.Identifier): ResolvedType | null { + return this.#bindings.get(bindingIdentifier) ?? null; + } + + pushGeneric(name: string, generic: TypeParameter): void { + this.#generics.unshift([name, generic]); + } + + popGeneric(name: string): void { + for (let i = 0; i < this.#generics.length; i++) { + if (this.#generics[i][0] === name) { + this.#generics.splice(i, 1); + return; + } + } + } + + /** + * Look up bound polymorphic types + * @param name + * @returns + */ + getGeneric(name: string): null | TypeParameter { + for (const [eltName, param] of this.#generics) { + if (name === eltName) { + return param; + } + } + return null; + } +} +const Primitives = { + number(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Number'}, platform}; + }, + string(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'String'}, platform}; + }, + boolean(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Boolean'}, platform}; + }, + void(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Void'}, platform}; + }, + mixed(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Mixed'}, platform}; + }, + enum(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Enum'}, platform}; + }, + todo(platform: Platform): Type & ResolvedType { + return {kind: 'Concrete', type: {kind: 'Mixed'}, platform}; + }, +}; + +export const Resolved = { + ...Primitives, + nullable(type: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Nullable', type}, platform}; + }, + array(element: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Array', element}, platform}; + }, + set(element: ResolvedType, platform: Platform): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Set', element}, platform}; + }, + map( + key: ResolvedType, + value: ResolvedType, + platform: Platform, + ): ResolvedType { + return {kind: 'Concrete', type: {kind: 'Map', key, value}, platform}; + }, + function( + typeParameters: null | Array>, + params: Array, + returnType: ResolvedType, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: {kind: 'Function', typeParameters, params, returnType}, + platform, + }; + }, + component( + props: Map, + children: ResolvedType | null, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: {kind: 'Component', props, children}, + platform, + }; + }, + object( + id: NominalId, + members: Map, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Object', + id, + members, + }, + platform, + }; + }, + class( + name: string, + members: Map, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Instance', + name, + members, + }, + platform, + }; + }, + tuple( + id: NominalId, + members: Array, + platform: Platform, + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Tuple', + id, + members, + }, + platform, + }; + }, + generic( + id: TypeParameterId, + platform: Platform, + bound = Primitives.mixed(platform), + ): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Generic', + id, + bound, + }, + platform, + }; + }, + union(members: Array, platform: Platform): ResolvedType { + return { + kind: 'Concrete', + type: { + kind: 'Union', + members, + }, + platform, + }; + }, +}; + +/* + * export const Types = { + * ...Primitives, + * variable(env: TypeEnv): Type { + * return env.nextTypeVariable(); + * }, + * nullable(type: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Nullable', type}}; + * }, + * array(element: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Array', element}}; + * }, + * set(element: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Set', element}}; + * }, + * map(key: Type, value: Type): Type { + * return {kind: 'Concrete', type: {kind: 'Map', key, value}}; + * }, + * function( + * typeParameters: null | Array>, + * params: Array, + * returnType: Type, + * ): Type { + * return { + * kind: 'Concrete', + * type: {kind: 'Function', typeParameters, params, returnType}, + * }; + * }, + * component( + * props: Map, + * children: Type | null, + * ): Type { + * return { + * kind: 'Concrete', + * type: {kind: 'Component', props, children}, + * }; + * }, + * object(id: NominalId, members: Map): Type { + * return { + * kind: 'Concrete', + * type: { + * kind: 'Object', + * id, + * members, + * }, + * }; + * }, + * }; + */ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 0ac59a8982e2f..957c5ab84ab28 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -49,6 +49,7 @@ import { } from './ObjectShape'; import {Scope as BabelScope, NodePath} from '@babel/traverse'; import {TypeSchema} from './TypeSchema'; +import {FlowTypeEnv} from '../Flood/Types'; export const ReactElementSymbolSchema = z.object({ elementSymbol: z.union([ @@ -243,6 +244,12 @@ export const EnvironmentConfigSchema = z.object({ */ enableUseTypeAnnotations: z.boolean().default(false), + /** + * Allows specifying a function that can populate HIR with type information from + * Flow + */ + flowTypeProvider: z.nullable(z.function().args(z.string())).default(null), + /** * Enable a new model for mutability and aliasing inference */ @@ -697,6 +704,8 @@ export class Environment { #hoistedIdentifiers: Set; parentFunction: NodePath; + #flowTypeEnvironment: FlowTypeEnv | null; + constructor( scope: BabelScope, fnType: ReactFunctionType, @@ -765,6 +774,26 @@ export class Environment { this.parentFunction = parentFunction; this.#contextIdentifiers = contextIdentifiers; this.#hoistedIdentifiers = new Set(); + + if (config.flowTypeProvider != null) { + this.#flowTypeEnvironment = new FlowTypeEnv(); + CompilerError.invariant(code != null, { + reason: + 'Expected Environment to be initialized with source code when a Flow type provider is specified', + loc: null, + }); + this.#flowTypeEnvironment.init(this, code); + } else { + this.#flowTypeEnvironment = null; + } + } + + get typeContext(): FlowTypeEnv { + CompilerError.invariant(this.#flowTypeEnvironment != null, { + reason: 'Flow type environment not initialized', + loc: null, + }); + return this.#flowTypeEnvironment; } get isInferredMemoEnabled(): boolean { diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts index ea2f798770525..a7acf5d66250f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/MergeReactiveScopesThatInvalidateTogether.ts @@ -504,7 +504,7 @@ function canMergeScopes( return false; } -function isAlwaysInvalidatingType(type: Type): boolean { +export function isAlwaysInvalidatingType(type: Type): boolean { switch (type.kind) { case 'Object': { switch (type.shapeId) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts index 449064ef3280a..566732c2bb9fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/DisjointSet.ts @@ -78,6 +78,10 @@ export default class DisjointSet { return root; } + has(item: T): boolean { + return this.#entries.has(item); + } + /* * Forces the set into canonical form, ie with all items pointing directly to * their root, and returns a Map representing the mapping of items to their roots. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts index e5fbacfc772df..897614015f544 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never { // Modifies @param array in place, retaining only the items where the predicate returns true. export function retainWhere( array: Array, - predicate: (item: T) => boolean, + predicate: (item: T, index: number) => boolean, ): void { let writeIndex = 0; for (let readIndex = 0; readIndex < array.length; readIndex++) { const item = array[readIndex]; - if (predicate(item) === true) { + if (predicate(item, readIndex) === true) { array[writeIndex++] = item; } } diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 189d504ad5125..aab9476db3074 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -271,6 +271,9 @@ function createVirtualInstance( type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance; +// A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native. +type Rect = {x: number, y: number, width: number, height: number, ...}; + type SuspenseNode = { // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot. // It can also be disconnected from the main tree if it's a Filtered Instance. @@ -278,6 +281,7 @@ type SuspenseNode = { parent: null | SuspenseNode, firstChild: null | SuspenseNode, nextSibling: null | SuspenseNode, + rects: null | Array, // The bounding rects of content children. suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all // also in the parent sets. This determine whether this could contribute in the loading sequence. @@ -292,6 +296,7 @@ function createSuspenseNode( parent: null, firstChild: null, nextSibling: null, + rects: null, suspendedBy: new Map(), hasUniqueSuspenders: false, }); @@ -2130,6 +2135,69 @@ export function attach( pendingStringTableLength = 0; } + function measureHostInstance(instance: HostInstance): null | Array { + // Feature detect measurement capabilities of this environment. + // TODO: Consider making this capability injected by the ReactRenderer. + if (typeof instance !== 'object' || instance === null) { + return null; + } + if (typeof instance.getClientRects === 'function') { + // DOM + const result = []; + const doc = instance.ownerDocument; + const win = doc && doc.defaultView; + const scrollX = win ? win.scrollX : 0; + const scrollY = win ? win.scrollY : 0; + const rects = instance.getClientRects(); + for (let i = 0; i < rects.length; i++) { + const rect = rects[i]; + result.push({ + x: rect.x + scrollX, + y: rect.y + scrollY, + width: rect.width, + height: rect.height, + }); + } + return result; + } + if (instance.canonical) { + // Native + const publicInstance = instance.canonical.publicInstance; + if (!publicInstance) { + // The publicInstance may not have been initialized yet if there was no ref on this node. + // We can't initialize it from any existing Hook but we could fallback to this async form: + // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback) + return null; + } + if (typeof publicInstance.getBoundingClientRect === 'function') { + // enableAccessToHostTreeInFabric / ReadOnlyElement + return [publicInstance.getBoundingClientRect()]; + } + if (typeof publicInstance.unstable_getBoundingClientRect === 'function') { + // ReactFabricHostComponent + return [publicInstance.unstable_getBoundingClientRect()]; + } + } + return null; + } + + function measureInstance(instance: DevToolsInstance): null | Array { + // Synchronously return the client rects of the Host instances directly inside this Instance. + const hostInstances = findAllCurrentHostInstances(instance); + let result: null | Array = null; + for (let i = 0; i < hostInstances.length; i++) { + const childResult = measureHostInstance(hostInstances[i]); + if (childResult !== null) { + if (result === null) { + result = childResult; + } else { + result = result.concat(childResult); + } + } + } + return result; + } + function getStringID(string: string | null): number { if (string === null) { return 0; @@ -2439,6 +2507,10 @@ export function attach( } } + function recordSuspenseResize(suspenseNode: SuspenseNode): void { + // TODO: Notify the front end of the change. + } + // Running state of the remaining children from the previous version of this parent that // we haven't yet added back. This should be reset anytime we change parent. // Any remaining ones at the end will be deleted. @@ -2768,6 +2840,79 @@ export function attach( return false; } + function areEqualRects( + a: null | Array, + b: null | Array, + ): boolean { + if (a === null) { + return b === null; + } + if (b === null) { + return false; + } + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + const aRect = a[i]; + const bRect = b[i]; + if ( + aRect.x !== bRect.x || + aRect.y !== bRect.y || + aRect.width !== bRect.width || + aRect.height !== bRect.height + ) { + return false; + } + } + return true; + } + + function measureUnchangedSuspenseNodesRecursively( + suspenseNode: SuspenseNode, + ): void { + if (isInDisconnectedSubtree) { + // We don't update rects inside disconnected subtrees. + return; + } + const nextRects = measureInstance(suspenseNode.instance); + const prevRects = suspenseNode.rects; + if (areEqualRects(prevRects, nextRects)) { + return; // Unchanged + } + // The rect has changed. While the bailed out root wasn't in a disconnected subtree, + // it's possible that this node was in one. So we need to check if we're offscreen. + let parent = suspenseNode.instance.parent; + while (parent !== null) { + if ( + (parent.kind === FIBER_INSTANCE || + parent.kind === FILTERED_FIBER_INSTANCE) && + parent.data.tag === OffscreenComponent && + parent.data.memoizedState !== null + ) { + // We're inside a hidden offscreen Fiber. We're in a disconnected tree. + return; + } + if (parent.suspenseNode !== null) { + // Found our parent SuspenseNode. We can bail out now. + break; + } + parent = parent.parent; + } + // We changed inside a visible tree. + // Since this boundary changed, it's possible it also affected its children so lets + // measure them as well. + for ( + let child = suspenseNode.firstChild; + child !== null; + child = child.nextSibling + ) { + measureUnchangedSuspenseNodesRecursively(child); + } + suspenseNode.rects = nextRects; + recordSuspenseResize(suspenseNode); + } + function consumeSuspenseNodesOfExistingInstance( instance: DevToolsInstance, ): void { @@ -2806,6 +2951,9 @@ export function attach( previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; } previouslyReconciledSiblingSuspenseNode = suspenseNode; + // While React didn't rerender this node, it's possible that it was affected by + // layout due to mutation of a parent or sibling. Check if it changed size. + measureUnchangedSuspenseNodesRecursively(suspenseNode); // Continue suspenseNode = nextRemainingSibling; } else if (foundOne) { @@ -3029,6 +3177,10 @@ export function attach( newInstance = recordMount(fiber, reconcilingParent); if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) { newSuspenseNode = createSuspenseNode(newInstance); + // Measure this Suspense node. In general we shouldn't do this until we have + // inserted the new children but since we know this is a FiberInstance we'll + // just use the Fiber anyway. + newSuspenseNode.rects = measureInstance(newInstance); } insertChild(newInstance); if (__DEBUG__) { @@ -3058,6 +3210,10 @@ export function attach( newInstance = createFilteredFiberInstance(fiber); if (fiber.tag === SuspenseComponent) { newSuspenseNode = createSuspenseNode(newInstance); + // Measure this Suspense node. In general we shouldn't do this until we have + // inserted the new children but since we know this is a FiberInstance we'll + // just use the Fiber anyway. + newSuspenseNode.rects = measureInstance(newInstance); } insertChild(newInstance); if (__DEBUG__) { @@ -4084,6 +4240,23 @@ export function attach( ) { shouldResetChildren = true; } + } else if ( + nextFiber.memoizedState === null && + fiberInstance.suspenseNode !== null + ) { + if (!isInDisconnectedSubtree) { + // Measure this Suspense node in case it changed. We don't update the rect while + // we're inside a disconnected subtree nor if we are the Suspense boundary that + // is suspended. This lets us keep the rectangle of the displayed content while + // we're suspended to visualize the resulting state. + const suspenseNode = fiberInstance.suspenseNode; + const prevRects = suspenseNode.rects; + const nextRects = measureInstance(fiberInstance); + if (!areEqualRects(prevRects, nextRects)) { + suspenseNode.rects = nextRects; + recordSuspenseResize(suspenseNode); + } + } } } else { // Common case: Primary -> Primary. @@ -4179,6 +4352,21 @@ export function attach( previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; if (shouldPopSuspenseNode) { + if ( + !isInDisconnectedSubtree && + reconcilingParentSuspenseNode !== null + ) { + // Measure this Suspense node in case it changed. We don't update the rect + // while we're inside a disconnected subtree so that we keep the outline + // as it was before we hid the parent. + const suspenseNode = reconcilingParentSuspenseNode; + const prevRects = suspenseNode.rects; + const nextRects = measureInstance(fiberInstance); + if (!areEqualRects(prevRects, nextRects)) { + suspenseNode.rects = nextRects; + recordSuspenseResize(suspenseNode); + } + } reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;