From 44b2f504ea3a1af837398f952bed323985f55749 Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Thu, 30 Mar 2023 15:03:10 -0400 Subject: [PATCH] [rfc][env] Hook, object, and function types for Environment globals Adds `GlobalRegistry`, which holds the names and types of known global objects, i.e. ```js type GlobalRegistry = Map; // ... globalRegistry.get("NaN"); // {kind: "Primitive"} globalRegistry.get("parseInt"); // {kind: "Function", shapeId: "..."} globalRegistry.get("Math"); // {kind: "Object", shapeId: "..."} ``` Since we currently do not track module imports and module-level declarations, builtin and custom hooks currently also live in GlobalRegistry. ```js globalRegistry.get("useState"); // {kind: "Hook", definition: {...}} globalRegistry.get("useFreeze"); // {kind: Hook, definition: {...}} ``` This PR does not allow Forget users to define their own globals. When we add this as a configuration, we should not expose `ShapeRegistry` to the user, as a user-provided ShapeRegistry may accidentally be not well formed. (i.e. missing (1) required shapes (BuiltInArray for [] and BuiltInObject for {}) or (2) some recursive shapeIds) ```js export type UserType = UserObject | UserFunction | "Primitive" | "BuiltinObject" | ...; export type UserObject = { kind: "Object", properties: Map } export type UserFunction = { kind: "Function", properties: Map, signature: ... } export type UserGlobals = Map; class Environment { constructor(globals: Map, ...) { // ... addUserDefinedGlobals(this.#globals, this.#shapes); ``` --- compiler/forget/src/HIR/Environment.ts | 68 +++++++++++------- compiler/forget/src/HIR/Globals.ts | 69 +++++++++++++++++-- compiler/forget/src/HIR/HIRBuilder.ts | 19 ++++- compiler/forget/src/HIR/Hooks.ts | 39 ----------- .../forget/src/TypeInference/InferTypes.ts | 10 +-- 5 files changed, 131 insertions(+), 74 deletions(-) diff --git a/compiler/forget/src/HIR/Environment.ts b/compiler/forget/src/HIR/Environment.ts index 28aa5abfaa..f9b73e8888 100644 --- a/compiler/forget/src/HIR/Environment.ts +++ b/compiler/forget/src/HIR/Environment.ts @@ -1,6 +1,6 @@ import invariant from "invariant"; import { log } from "../Utils/logger"; -import { DEFAULT_GLOBALS, Global } from "./Globals"; +import { DEFAULT_GLOBALS, Global, GlobalRegistry } from "./Globals"; import { BuiltInType, Effect, @@ -10,7 +10,7 @@ import { ObjectType, ValueKind, } from "./HIR"; -import { BUILTIN_HOOKS, Hook } from "./Hooks"; +import { Hook } from "./Hooks"; import { BUILTIN_SHAPES, FunctionSignature, @@ -19,21 +19,41 @@ import { const HOOK_PATTERN = /^_?use/; +// TODO(mofeiZ): User defined global types (with corresponding shapes). +// User defined global types should have inline ObjectShapes instead of directly +// using ObjectShapes.ShapeRegistry, as a user-provided ShapeRegistry may be +// accidentally be not well formed. +// i.e. +// missing required shapes (BuiltInArray for [] and BuiltInObject for {}) +// missing some recursive Object / Function shapeIds export type EnvironmentConfig = Partial<{ customHooks: Map; memoizeJsxElements: boolean; }>; export class Environment { - #customHooks: Map; - #globals: Set; + #globals: GlobalRegistry; #shapes: ShapeRegistry; #nextIdentifer: number = 0; constructor(config: EnvironmentConfig | null) { - this.#customHooks = config?.customHooks ?? new Map(); this.#shapes = BUILTIN_SHAPES; - this.#globals = DEFAULT_GLOBALS; + + if (config?.customHooks) { + this.#globals = new Map(DEFAULT_GLOBALS); + for (const [hookName, hook] of config.customHooks) { + invariant( + !this.#globals.has(hookName), + `[Globals] Found existing definition in global registry for custom hook ${hookName}` + ); + this.#globals.set(hookName, { + kind: "Hook", + definition: hook, + }); + } + } else { + this.#globals = DEFAULT_GLOBALS; + } } get nextIdentifierId(): IdentifierId { @@ -41,26 +61,24 @@ export class Environment { } getGlobalDeclaration(name: string): Global | null { - if (!this.#globals.has(name)) { - log(() => `Undefined global '${name}'`); + let resolvedGlobal: Global | null = this.#globals.get(name) ?? null; + if (resolvedGlobal === null) { + // Hack, since we don't track module level declarations and imports + if (name.match(HOOK_PATTERN)) { + return { + kind: "Hook", + definition: { + kind: "Custom", + name, + effectKind: Effect.Mutate, + valueKind: ValueKind.Mutable, + }, + }; + } else { + log(() => `Undefined global '${name}'`); + } } - return { name }; - } - - getHookDeclaration(name: string): Hook | null { - if (!name.match(HOOK_PATTERN)) { - return null; - } - const hook = BUILTIN_HOOKS.get(name) ?? this.#customHooks.get(name); - if (hook !== undefined) { - return hook; - } - return { - kind: "Custom", - name, - effectKind: Effect.Mutate, - valueKind: ValueKind.Mutable, - }; + return resolvedGlobal; } getPropertyType( diff --git a/compiler/forget/src/HIR/Globals.ts b/compiler/forget/src/HIR/Globals.ts index 5e868d234a..e56d73b6c2 100644 --- a/compiler/forget/src/HIR/Globals.ts +++ b/compiler/forget/src/HIR/Globals.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -export const DEFAULT_GLOBALS: Set = new Set([ +import { Effect, ValueKind } from "./HIR"; +import { Hook } from "./Hooks"; +import { BuiltInType, HookType, PolyType } from "./Types"; + +// Hack until we add ObjectShapes for all globals +const UNTYPED_GLOBALS: Set = new Set([ "String", "Object", "Function", @@ -50,6 +55,62 @@ export const DEFAULT_GLOBALS: Set = new Set([ "decodeURIComponent", ]); -export type Global = { - name: string; -}; +const BUILTIN_HOOKS: Array<[string, Hook]> = [ + [ + "useState", + { + kind: "State", + name: "useState", + effectKind: Effect.Freeze, + valueKind: ValueKind.Frozen, + }, + ], + [ + "useRef", + { + kind: "Ref", + name: "useRef", + effectKind: Effect.Capture, + valueKind: ValueKind.Mutable, + }, + ], + [ + "useMemo", + { + kind: "Memo", + name: "useMemo", + effectKind: Effect.Freeze, + valueKind: ValueKind.Frozen, + }, + ], + [ + "useCallback", + { + kind: "Memo", + name: "useCallback", + effectKind: Effect.Freeze, + valueKind: ValueKind.Frozen, + }, + ], +]; + +export type Global = BuiltInType | HookType | PolyType; +export type GlobalRegistry = Map; +export const DEFAULT_GLOBALS: GlobalRegistry = new Map( + BUILTIN_HOOKS.map(([hookName, hook]) => { + return [ + hookName, + { + kind: "Hook", + definition: hook, + }, + ]; + }) +); + +// Hack until we add ObjectShapes for all globals +for (const name of UNTYPED_GLOBALS) { + DEFAULT_GLOBALS.set(name, { + kind: "Poly", + }); +} diff --git a/compiler/forget/src/HIR/HIRBuilder.ts b/compiler/forget/src/HIR/HIRBuilder.ts index 3671265730..76a2c61f6e 100644 --- a/compiler/forget/src/HIR/HIRBuilder.ts +++ b/compiler/forget/src/HIR/HIRBuilder.ts @@ -143,8 +143,23 @@ export default class HIRBuilder { }; } - resolveGlobal(path: NodePath): Global | null { - return this.#env.getGlobalDeclaration(path.node.name); + resolveGlobal( + path: NodePath + ): (Global & { name: string }) | null { + const name = path.node.name; + const resolvedGlobal = this.#env.getGlobalDeclaration(name); + if (resolvedGlobal) { + return { + ...resolvedGlobal, + name, + }; + } else { + // if env records no global with the given name, load it as an unknown type + return { + kind: "Poly", + name, + }; + } } /** diff --git a/compiler/forget/src/HIR/Hooks.ts b/compiler/forget/src/HIR/Hooks.ts index 9ea29c5536..20fb7d5c81 100644 --- a/compiler/forget/src/HIR/Hooks.ts +++ b/compiler/forget/src/HIR/Hooks.ts @@ -7,45 +7,6 @@ import { Effect, ValueKind } from "./HIR"; -export const BUILTIN_HOOKS: Map = new Map([ - [ - "useState", - { - kind: "State", - name: "useState", - effectKind: Effect.Freeze, - valueKind: ValueKind.Frozen, - }, - ], - [ - "useRef", - { - kind: "Ref", - name: "useRef", - effectKind: Effect.Capture, - valueKind: ValueKind.Mutable, - }, - ], - [ - "useMemo", - { - kind: "Memo", - name: "useMemo", - effectKind: Effect.Freeze, - valueKind: ValueKind.Frozen, - }, - ], - [ - "useCallback", - { - kind: "Memo", - name: "useCallback", - effectKind: Effect.Freeze, - valueKind: ValueKind.Frozen, - }, - ], -]); - export type HookKind = "State" | "Ref" | "Custom" | "Memo"; export type Hook = { kind: HookKind; diff --git a/compiler/forget/src/TypeInference/InferTypes.ts b/compiler/forget/src/TypeInference/InferTypes.ts index 655ea68c56..89693b5a99 100644 --- a/compiler/forget/src/TypeInference/InferTypes.ts +++ b/compiler/forget/src/TypeInference/InferTypes.ts @@ -135,10 +135,12 @@ function* generateInstructionTypes( } case "LoadGlobal": { - const hook = env.getHookDeclaration(value.name); - if (hook !== null) { - const type: Type = { kind: "Hook", definition: hook }; - yield equation(left, type); + const globalType = env.getGlobalDeclaration(value.name); + if (globalType) { + if (globalType.kind === "Hook") { + yield equation(left, globalType); + } + // TODO(mofeiZ): add type inference for other globals } break; }