@samitouri / QOS-React-2 / commits / 5ba0a03e00

[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 }