@samitouri / QOS-React-2 / commits / 44b2f504ea

[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<string, PrimitiveType | ObjectType | FunctionType | HookType | PolyType>; // ... 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<string, UserType> } export type UserFunction = { kind: "Function", properties: Map<string, UserType>, signature: ... } export type UserGlobals = Map<string, UserType>; class Environment { constructor(globals: Map<string, UserType>, ...) { // ... addUserDefinedGlobals(this.#globals, this.#shapes); ```

Mofei Zhang committed Mar 30, 2023 at 15:03 UTC 44b2f504ea3a1af837398f952bed323985f55749
5 files changed +131 -74
compiler/forget/src/HIR/Environment.ts
+43 -25
@@ -1,6 +1,6 @@
1 import invariant from "invariant";
2 import { log } from "../Utils/logger";
3 -import { DEFAULT_GLOBALS, Global } from "./Globals";
3 +import { DEFAULT_GLOBALS, Global, GlobalRegistry } from "./Globals";
4 import {
5 BuiltInType,
6 Effect,
@@ -10,7 +10,7 @@ import {
10 ObjectType,
11 ValueKind,
12 } from "./HIR";
13 -import { BUILTIN_HOOKS, Hook } from "./Hooks";
13 +import { Hook } from "./Hooks";
14 import {
15 BUILTIN_SHAPES,
16 FunctionSignature,
@@ -19,21 +19,41 @@ import {
19
20 const HOOK_PATTERN = /^_?use/;
21
22 +// TODO(mofeiZ): User defined global types (with corresponding shapes).
23 +// User defined global types should have inline ObjectShapes instead of directly
24 +// using ObjectShapes.ShapeRegistry, as a user-provided ShapeRegistry may be
25 +// accidentally be not well formed.
26 +// i.e.
27 +// missing required shapes (BuiltInArray for [] and BuiltInObject for {})
28 +// missing some recursive Object / Function shapeIds
29 export type EnvironmentConfig = Partial<{
30 customHooks: Map<string, Hook>;
31 memoizeJsxElements: boolean;
32 }>;
33
34 export class Environment {
28 - #customHooks: Map<string, Hook>;
29 - #globals: Set<string>;
35 + #globals: GlobalRegistry;
36 #shapes: ShapeRegistry;
37 #nextIdentifer: number = 0;
38
39 constructor(config: EnvironmentConfig | null) {
34 - this.#customHooks = config?.customHooks ?? new Map();
40 this.#shapes = BUILTIN_SHAPES;
36 - this.#globals = DEFAULT_GLOBALS;
41 +
42 + if (config?.customHooks) {
43 + this.#globals = new Map(DEFAULT_GLOBALS);
44 + for (const [hookName, hook] of config.customHooks) {
45 + invariant(
46 + !this.#globals.has(hookName),
47 + `[Globals] Found existing definition in global registry for custom hook ${hookName}`
48 + );
49 + this.#globals.set(hookName, {
50 + kind: "Hook",
51 + definition: hook,
52 + });
53 + }
54 + } else {
55 + this.#globals = DEFAULT_GLOBALS;
56 + }
57 }
58
59 get nextIdentifierId(): IdentifierId {
@@ -41,26 +61,24 @@ export class Environment {
61 }
62
63 getGlobalDeclaration(name: string): Global | null {
44 - if (!this.#globals.has(name)) {
45 - log(() => `Undefined global '${name}'`);
46 - }
47 - return { name };
48 - }
49 -
50 - getHookDeclaration(name: string): Hook | null {
51 - if (!name.match(HOOK_PATTERN)) {
52 - return null;
53 - }
54 - const hook = BUILTIN_HOOKS.get(name) ?? this.#customHooks.get(name);
55 - if (hook !== undefined) {
56 - return hook;
64 + let resolvedGlobal: Global | null = this.#globals.get(name) ?? null;
65 + if (resolvedGlobal === null) {
66 + // Hack, since we don't track module level declarations and imports
67 + if (name.match(HOOK_PATTERN)) {
68 + return {
69 + kind: "Hook",
70 + definition: {
71 + kind: "Custom",
72 + name,
73 + effectKind: Effect.Mutate,
74 + valueKind: ValueKind.Mutable,
75 + },
76 + };
77 + } else {
78 + log(() => `Undefined global '${name}'`);
79 + }
80 }
58 - return {
59 - kind: "Custom",
60 - name,
61 - effectKind: Effect.Mutate,
62 - valueKind: ValueKind.Mutable,
63 - };
81 + return resolvedGlobal;
82 }
83
84 getPropertyType(
compiler/forget/src/HIR/Globals.ts
+65 -4
@@ -5,7 +5,12 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -export const DEFAULT_GLOBALS: Set<string> = new Set([
8 +import { Effect, ValueKind } from "./HIR";
9 +import { Hook } from "./Hooks";
10 +import { BuiltInType, HookType, PolyType } from "./Types";
11 +
12 +// Hack until we add ObjectShapes for all globals
13 +const UNTYPED_GLOBALS: Set<string> = new Set([
14 "String",
15 "Object",
16 "Function",
@@ -50,6 +55,62 @@ export const DEFAULT_GLOBALS: Set<string> = new Set([
55 "decodeURIComponent",
56 ]);
57
53 -export type Global = {
54 - name: string;
55 -};
58 +const BUILTIN_HOOKS: Array<[string, Hook]> = [
59 + [
60 + "useState",
61 + {
62 + kind: "State",
63 + name: "useState",
64 + effectKind: Effect.Freeze,
65 + valueKind: ValueKind.Frozen,
66 + },
67 + ],
68 + [
69 + "useRef",
70 + {
71 + kind: "Ref",
72 + name: "useRef",
73 + effectKind: Effect.Capture,
74 + valueKind: ValueKind.Mutable,
75 + },
76 + ],
77 + [
78 + "useMemo",
79 + {
80 + kind: "Memo",
81 + name: "useMemo",
82 + effectKind: Effect.Freeze,
83 + valueKind: ValueKind.Frozen,
84 + },
85 + ],
86 + [
87 + "useCallback",
88 + {
89 + kind: "Memo",
90 + name: "useCallback",
91 + effectKind: Effect.Freeze,
92 + valueKind: ValueKind.Frozen,
93 + },
94 + ],
95 +];
96 +
97 +export type Global = BuiltInType | HookType | PolyType;
98 +export type GlobalRegistry = Map<string, Global>;
99 +export const DEFAULT_GLOBALS: GlobalRegistry = new Map(
100 + BUILTIN_HOOKS.map(([hookName, hook]) => {
101 + return [
102 + hookName,
103 + {
104 + kind: "Hook",
105 + definition: hook,
106 + },
107 + ];
108 + })
109 +);
110 +
111 +// Hack until we add ObjectShapes for all globals
112 +for (const name of UNTYPED_GLOBALS) {
113 + DEFAULT_GLOBALS.set(name, {
114 + kind: "Poly",
115 + });
116 +}
compiler/forget/src/HIR/HIRBuilder.ts
+17 -2
@@ -143,8 +143,23 @@ export default class HIRBuilder {
143 };
144 }
145
146 - resolveGlobal(path: NodePath<t.Identifier | t.JSXIdentifier>): Global | null {
147 - return this.#env.getGlobalDeclaration(path.node.name);
146 + resolveGlobal(
147 + path: NodePath<t.Identifier | t.JSXIdentifier>
148 + ): (Global & { name: string }) | null {
149 + const name = path.node.name;
150 + const resolvedGlobal = this.#env.getGlobalDeclaration(name);
151 + if (resolvedGlobal) {
152 + return {
153 + ...resolvedGlobal,
154 + name,
155 + };
156 + } else {
157 + // if env records no global with the given name, load it as an unknown type
158 + return {
159 + kind: "Poly",
160 + name,
161 + };
162 + }
163 }
164
165 /**
compiler/forget/src/HIR/Hooks.ts
-39
@@ -7,45 +7,6 @@
7
8 import { Effect, ValueKind } from "./HIR";
9
10 -export const BUILTIN_HOOKS: Map<string, Hook> = new Map([
11 - [
12 - "useState",
13 - {
14 - kind: "State",
15 - name: "useState",
16 - effectKind: Effect.Freeze,
17 - valueKind: ValueKind.Frozen,
18 - },
19 - ],
20 - [
21 - "useRef",
22 - {
23 - kind: "Ref",
24 - name: "useRef",
25 - effectKind: Effect.Capture,
26 - valueKind: ValueKind.Mutable,
27 - },
28 - ],
29 - [
30 - "useMemo",
31 - {
32 - kind: "Memo",
33 - name: "useMemo",
34 - effectKind: Effect.Freeze,
35 - valueKind: ValueKind.Frozen,
36 - },
37 - ],
38 - [
39 - "useCallback",
40 - {
41 - kind: "Memo",
42 - name: "useCallback",
43 - effectKind: Effect.Freeze,
44 - valueKind: ValueKind.Frozen,
45 - },
46 - ],
47 -]);
48 -
10 export type HookKind = "State" | "Ref" | "Custom" | "Memo";
11 export type Hook = {
12 kind: HookKind;
compiler/forget/src/TypeInference/InferTypes.ts
+6 -4
@@ -135,10 +135,12 @@ function* generateInstructionTypes(
135 }
136
137 case "LoadGlobal": {
138 - const hook = env.getHookDeclaration(value.name);
139 - if (hook !== null) {
140 - const type: Type = { kind: "Hook", definition: hook };
141 - yield equation(left, type);
138 + const globalType = env.getGlobalDeclaration(value.name);
139 + if (globalType) {
140 + if (globalType.kind === "Hook") {
141 + yield equation(left, globalType);
142 + }
143 + // TODO(mofeiZ): add type inference for other globals
144 }
145 break;
146 }