From fa212fc2b14c2262849c8eab01e7c1895cb20c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 6 Aug 2025 14:56:52 -0400 Subject: [PATCH 1/2] [DevTools] Measure the Rectangle of Suspense boundaries as we reconcile (#34090) Stacked on #34089. This measures the client rects of the direct children of Suspense boundaries as we reconcile. This will be used by the Suspense tab to visualize the boundaries given their outlines. We could ask for this more lazily just in case we're currently looking at the Suspense tab. We could also do something like monitor the sizes using a ResizeObserver to cover when they change. However, it should be pretty cheap to this in the reconciliation phase since we're already mostly visiting these nodes on the way down. We have also already done all the layouts at this point since it was part of the commit phase and paint already. So we're just reading cached values in this phase. We can also infer that things are expected to change when parents or sibling changes. Similar technique as ViewTransitions. --- .../src/backend/fiber/renderer.js | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) 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; From c403a7c54805e1198bfdad7fc97f33a792359a77 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:58:07 -0700 Subject: [PATCH 2/2] [compiler] Upstream experimental flow integration (#34121) all credit on the Flood/ code goes to @mvitousek and @jbrown215, i'm just the one upstreaming it --- .../src/Flood/FlowTypes.ts | 752 +++++++++++++ .../src/Flood/TypeErrors.ts | 131 +++ .../src/Flood/TypeUtils.ts | 312 +++++ .../src/Flood/Types.ts | 1001 +++++++++++++++++ .../src/HIR/Environment.ts | 29 + ...rgeReactiveScopesThatInvalidateTogether.ts | 2 +- .../src/Utils/DisjointSet.ts | 4 + .../src/Utils/utils.ts | 4 +- 8 files changed, 2232 insertions(+), 3 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Flood/FlowTypes.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Flood/TypeErrors.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Flood/TypeUtils.ts create mode 100644 compiler/packages/babel-plugin-react-compiler/src/Flood/Types.ts 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; } }