[config] Clean up codegen imports and gating
- Gating checks for debugging / profiling can be moved to within the instrumentation or makeReadOnly functions. This simplifies codegen and reduces code bloat. - Warn if importing conflicting identifiers - Aggregate imports from the same source
Mofei Zhang committed
Jun 5, 2023 at 14:46 UTC
5ba0a03e00f3bc1f1225989cd168d037d74a194f
15 files changed
+154
-94
compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Instrumentation.ts
+1
-6
@@ -11,18 +11,13 @@ import * as t from "@babel/types";
11
export function addInstrumentForget(
12
fn: NodePath<t.FunctionDeclaration>,
13
fnName: string,
14
- gatingIdentifierName: string,
14
instrumentFnName: string
15
): void {
16
const fnBody = fn.get("body");
17
// Technically, this is a conditional hook call. However, we expect
18
// __DEV__ and gatingIdentifier to be runtime constants
19
const testExpr: t.Node = t.ifStatement(
21
- t.logicalExpression(
22
- "&&",
23
- t.identifier("__DEV__"),
24
- t.identifier(gatingIdentifierName)
25
- ),
20
+ t.identifier("__DEV__"),
21
t.expressionStatement(
22
t.callExpression(t.identifier(instrumentFnName), [
23
t.stringLiteral(fnName),
compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Options.ts
+7
-14
@@ -55,33 +55,26 @@ export type PluginOptions = {
55
*/
56
gating: ExternalFunction | null;
57
/**
58
- * Enables instrumentation codegen. This emits a dev-mode only + gated call to
59
- * an instrumentation function, for components and hooks that Forget compiles.
58
+ * Enables instrumentation codegen. This emits a dev-mode only call to an
59
+ * instrumentation function, for components and hooks that Forget compiles.
60
* For example:
61
* instrumentForget: {
62
- * gating: {
63
- * source: 'ReactInstrumentForgetFeatureFlag',
64
- * importSpecifierName: 'isInstrumentForgetEnabled_Pokes',
65
- * },
66
- * instrumentFn: {
67
- * source: 'react-forget-runtime',
68
- * importSpecifierName: 'useRenderCounter',
69
- * }
62
+ * source: 'react-forget-runtime',
63
+ * importSpecifierName: 'useRenderCounter',
64
* }
65
*
66
* produces:
73
- * import {isInstrumentForgetEnabled_Pokes} from 'ReactInstrumentForgetFeatureFlag';
74
- * import {useRenderCounter} from 'react-forget-runtime';
67
+ * import {useRenderCounter} from 'react-forget-runtime-pokes';
68
*
69
* function Component(props) {
77
- * if (__DEV__ && isInstrumentForgetEnabled_Pokes) {
70
+ * if (__DEV__) {
71
* useRenderCounter();
72
* }
73
* // ...
74
* }
75
*
76
*/
84
- instrumentForget: InstrumentForgetOptions | null;
77
+ instrumentForget: ExternalFunction | null;
78
79
panicOnBailout: boolean;
80
compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+55
-55
@@ -16,6 +16,7 @@ import { GeneratedSource } from "../HIR";
16
import { addInstrumentForget } from "./Instrumentation";
17
import { ExternalFunction, PluginOptions, parsePluginOptions } from "./Options";
18
import { compileFn } from "./Pipeline";
19
+import { getOrInsertDefault } from "../Utils/utils";
20
21
export type CompilerPass = {
22
opts: PluginOptions;
@@ -81,36 +82,17 @@ export function compileProgram(
82
})
83
);
84
if (pass.opts.instrumentForget != null) {
84
- const gatingIdentifierName =
85
- pass.opts.instrumentForget.gating.importSpecifierName;
85
const instrumentFnName =
87
- pass.opts.instrumentForget.instrumentFn.importSpecifierName;
88
- addInstrumentForget(
89
- fn,
90
- originalIdent.name,
91
- gatingIdentifierName,
92
- instrumentFnName
93
- );
94
- addInstrumentForget(
95
- compiledFn,
96
- originalIdent.name,
97
- gatingIdentifierName,
98
- instrumentFnName
99
- );
86
+ pass.opts.instrumentForget.importSpecifierName;
87
+ addInstrumentForget(fn, originalIdent.name, instrumentFnName);
88
+ addInstrumentForget(compiledFn, originalIdent.name, instrumentFnName);
89
}
90
} else {
91
fn.replaceWith(compiled);
92
if (pass.opts.instrumentForget != null) {
104
- const gatingIdentifierName =
105
- pass.opts.instrumentForget.gating.importSpecifierName;
93
const instrumentFnName =
107
- pass.opts.instrumentForget.instrumentFn.importSpecifierName;
108
- addInstrumentForget(
109
- fn,
110
- originalIdent.name,
111
- gatingIdentifierName,
112
- instrumentFnName
113
- );
94
+ pass.opts.instrumentForget.importSpecifierName;
95
+ addInstrumentForget(fn, originalIdent.name, instrumentFnName);
96
}
97
}
98
@@ -315,29 +297,18 @@ export function compileProgram(
297
);
298
}
299
}
300
+ const externalFunctions = [];
301
// TODO: check for duplicate import specifiers
302
if (options.gating != null) {
320
- program.unshiftContainer(
321
- "body",
322
- buildImportForExternalFunction(options.gating)
323
- );
303
+ externalFunctions.push(options.gating);
304
}
305
if (options.instrumentForget != null) {
326
- program.unshiftContainer(
327
- "body",
328
- buildImportForExternalFunction(options.instrumentForget.gating)
329
- );
330
- program.unshiftContainer(
331
- "body",
332
- buildImportForExternalFunction(options.instrumentForget.instrumentFn)
333
- );
306
+ externalFunctions.push(options.instrumentForget);
307
}
308
if (options.environment?.enableEmitFreeze != null) {
336
- program.unshiftContainer(
337
- "body",
338
- buildImportForExternalFunction(options.environment?.enableEmitFreeze)
339
- );
309
+ externalFunctions.push(options.environment.enableEmitFreeze);
310
}
311
+ addImportsToProgram(program, externalFunctions);
312
}
313
}
314
@@ -453,6 +424,49 @@ function buildBlockStatement(
424
return body.node;
425
}
426
427
+function addImportsToProgram(
428
+ path: NodePath<t.Program>,
429
+ importList: Array<ExternalFunction>
430
+): void {
431
+ const identifiers: Set<string> = new Set();
432
+ const sortedImports: Map<string, Array<string>> = new Map();
433
+ for (const { importSpecifierName, source } of importList) {
434
+ // Codegen currently does not rename import specifiers, so we do additional
435
+ // validation here
436
+ if (identifiers.has(importSpecifierName)) {
437
+ CompilerError.invalidInput(
438
+ `[InvalidConfig] Encountered conflicting import specifier for ${importSpecifierName} in Forget config.`,
439
+ GeneratedSource
440
+ );
441
+ }
442
+ if (path.scope.hasBinding(importSpecifierName)) {
443
+ CompilerError.invalidInput(
444
+ `[InvalidConfig] Encountered conflicting import specifiers for ${importSpecifierName} in generated program.`,
445
+ GeneratedSource
446
+ );
447
+ }
448
+ identifiers.add(importSpecifierName);
449
+
450
+ const importSpecifierNameList = getOrInsertDefault(
451
+ sortedImports,
452
+ source,
453
+ []
454
+ );
455
+ importSpecifierNameList.push(importSpecifierName);
456
+ }
457
+
458
+ const stmts: Array<t.ImportDeclaration> = [];
459
+ for (const [source, importSpecifierNameList] of sortedImports) {
460
+ const importSpecifiers = importSpecifierNameList.map((name) => {
461
+ const id = t.identifier(name);
462
+ return t.importSpecifier(id, id);
463
+ });
464
+
465
+ stmts.push(t.importDeclaration(importSpecifiers, t.stringLiteral(source)));
466
+ }
467
+ path.unshiftContainer("body", stmts);
468
+}
469
+
470
type GatingTestOptions = {
471
originalFnDecl: NodePath<t.FunctionDeclaration>;
472
compiledIdent: t.Identifier;
@@ -469,7 +483,7 @@ function buildGatingTest({
483
t.variableDeclarator(
484
originalIdent,
485
t.conditionalExpression(
472
- t.callExpression(buildSpecifierIdent(gating), []),
486
+ t.callExpression(t.identifier(gating.importSpecifierName), []),
487
compiledIdent,
488
originalFnDecl.node.id!
489
)
@@ -500,20 +514,6 @@ function addSuffix(id: t.Identifier, suffix: string): t.Identifier {
514
return t.identifier(`${id.name}${suffix}`);
515
}
516
503
-function buildImportForExternalFunction(
504
- gating: ExternalFunction
505
-): t.ImportDeclaration {
506
- const specifierIdent = buildSpecifierIdent(gating);
507
- return t.importDeclaration(
508
- [t.importSpecifier(specifierIdent, specifierIdent)],
509
- t.stringLiteral(gating.source)
510
- );
511
-}
512
-
513
-function buildSpecifierIdent(gating: ExternalFunction): t.Identifier {
514
- return t.identifier(gating.importSpecifierName);
515
-}
516
-
517
/**
518
* Matches `import { ... } from 'react';`
519
* but not `import * as React from 'react';`
compiler/forget/packages/babel-plugin-react-forget/src/Utils/utils.ts
+13
@@ -44,3 +44,16 @@ export function retainWhere<T>(
44
}
45
array.length = writeIndex;
46
}
47
+
48
+export function getOrInsertDefault<U, V>(
49
+ m: Map<U, V>,
50
+ key: U,
51
+ defaultValue: V
52
+): V {
53
+ if (m.has(key)) {
54
+ return m.get(key) as V;
55
+ } else {
56
+ m.set(key, defaultValue);
57
+ return defaultValue;
58
+ }
59
+}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.expect.md
new
+35
@@ -0,0 +1,35 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableEmitFreeze @instrumentForget
6
+
7
+function useFoo(props) {
8
+ return foo(props.x);
9
+}
10
+
11
+```
12
+
13
+## Code
14
+
15
+```javascript
16
+import { useRenderCounter, makeReadOnly } from "react-forget-runtime";
17
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEmitFreeze @instrumentForget
18
+
19
+function useFoo(props) {
20
+ if (__DEV__) useRenderCounter("useFoo");
21
+ const $ = useMemoCache(2);
22
+ const c_0 = $[0] !== props.x;
23
+ let t0;
24
+ if (c_0) {
25
+ t0 = foo(props.x);
26
+ $[0] = props.x;
27
+ $[1] = __DEV__ ? makeReadOnly(t0, "useFoo") : t0;
28
+ } else {
29
+ t0 = $[1];
30
+ }
31
+ return t0;
32
+}
33
+
34
+```
35
+
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-emit-imports-same-source.js
new
+5
@@ -0,0 +1,5 @@
1
+// @enableEmitFreeze @instrumentForget
2
+
3
+function useFoo(props) {
4
+ return foo(props.x);
5
+}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-emit-make-read-only.expect.md
renamed
+1
-1
@@ -19,7 +19,7 @@ function MyComponentName(props) {
19
## Code
20
21
```javascript
22
-import { makeReadOnly } from "react-forget-runtime-emit-freeze";
22
+import { makeReadOnly } from "react-forget-runtime";
23
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEmitFreeze true
24
25
function MyComponentName(props) {
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-emit-make-read-only.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.expect.md
renamed
+5
-6
@@ -23,18 +23,17 @@ function Foo(props) {
23
## Code
24
25
```javascript
26
-import { useRenderCounter } from "react-forget-runtime";
27
-import { isInstrumentForgetEnabled_Fixtures } from "ReactInstrumentForgetFeatureFlag";
26
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
27
+import { useRenderCounter } from "react-forget-runtime";
28
import { unstable_useMemoCache as useMemoCache } from "react"; // @instrumentForget @forgetDirective @gating
29
30
function Bar_uncompiled(props) {
31
"use forget";
33
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Bar");
32
+ if (__DEV__) useRenderCounter("Bar");
33
return <div>{props.bar}</div>;
34
}
35
function Bar_forget(props) {
37
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Bar");
36
+ if (__DEV__) useRenderCounter("Bar");
37
const $ = useMemoCache(2);
38
const c_0 = $[0] !== props.bar;
39
let t0;
@@ -55,11 +54,11 @@ function NoForget(props) {
54
55
function Foo_uncompiled(props) {
56
"use forget";
58
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Foo");
57
+ if (__DEV__) useRenderCounter("Foo");
58
return <Foo>{props.bar}</Foo>;
59
}
60
function Foo_forget(props) {
62
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Foo");
61
+ if (__DEV__) useRenderCounter("Foo");
62
const $ = useMemoCache(2);
63
const c_0 = $[0] !== props.bar;
64
let t0;
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.expect.md
renamed
+2
-3
@@ -24,11 +24,10 @@ function Foo(props) {
24
25
```javascript
26
import { useRenderCounter } from "react-forget-runtime";
27
-import { isInstrumentForgetEnabled_Fixtures } from "ReactInstrumentForgetFeatureFlag";
27
import { unstable_useMemoCache as useMemoCache } from "react"; // @instrumentForget @forgetDirective
28
29
function Bar(props) {
31
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Bar");
30
+ if (__DEV__) useRenderCounter("Bar");
31
const $ = useMemoCache(2);
32
const c_0 = $[0] !== props.bar;
33
let t0;
@@ -47,7 +46,7 @@ function NoForget(props) {
46
}
47
48
function Foo(props) {
50
- if (__DEV__ && isInstrumentForgetEnabled_Fixtures) useRenderCounter("Foo");
49
+ if (__DEV__) useRenderCounter("Foo");
50
const $ = useMemoCache(2);
51
const c_0 = $[0] !== props.bar;
52
let t0;
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-instrument-forget-test.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.codegen-error-on-conflicting-imports.expect.md
new
+21
@@ -0,0 +1,21 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableEmitFreeze @instrumentForget
6
+
7
+let makeReadOnly = "conflicting identifier";
8
+function useFoo(props) {
9
+ return foo(props.x);
10
+}
11
+
12
+```
13
+
14
+
15
+## Error
16
+
17
+```
18
+[ReactForget] InvalidInput: [InvalidConfig] Encountered conflicting import specifiers for makeReadOnly in generated program.
19
+```
20
+
21
+
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.codegen-error-on-conflicting-imports.js
new
+6
@@ -0,0 +1,6 @@
1
+// @enableEmitFreeze @instrumentForget
2
+
3
+let makeReadOnly = "conflicting identifier";
4
+function useFoo(props) {
5
+ return foo(props.x);
6
+}
compiler/forget/packages/snap/src/compiler-worker.ts
+3
-9
@@ -109,14 +109,8 @@ export async function compile(
109
}
110
if (firstLine.indexOf("@instrumentForget") !== -1) {
111
instrumentForget = {
112
- gating: {
113
- source: "ReactInstrumentForgetFeatureFlag",
114
- importSpecifierName: "isInstrumentForgetEnabled_Fixtures",
115
- },
116
- instrumentFn: {
117
- source: "react-forget-runtime",
118
- importSpecifierName: "useRenderCounter",
119
- },
112
+ source: "react-forget-runtime",
113
+ importSpecifierName: "useRenderCounter",
114
};
115
}
116
if (firstLine.indexOf("@panicOnBailout false") !== -1) {
@@ -139,7 +133,7 @@ export async function compile(
133
}
134
if (firstLine.indexOf("@enableEmitFreeze") !== -1) {
135
enableEmitFreeze = {
142
- source: "react-forget-runtime-emit-freeze",
136
+ source: "react-forget-runtime",
137
importSpecifierName: "makeReadOnly",
138
};
139
}