@samitouri / QOS-React-1 / commits / 37e1975050

[be] Move codegen logic Program.ts -> Codegen

--- Prior to this PR, we were mutating functions after CodegenReactiveFunction completes (in `Entrypoint/Program.ts`). The reasoning for this separation was that we wanted to keep non-compiler logic out of the core Pipeline. However, it made our code difficult to read and reason about. Open to other alternatives, like adding a pass after Codegen.

Mofei Zhang committed Dec 5, 2023 at 15:02 UTC 37e1975050bad30f1170f58973a9fdbb5bdf4565
10 files changed +62 -90
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Instrumentation.ts deleted
-47
@@ -1,47 +0,0 @@
1 -/*
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import * as t from "@babel/types";
9 -import { assertExhaustive } from "../Utils/utils";
10 -
11 -export function addInstrumentForget(
12 - fn: t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression,
13 - instrumentFnName: string
14 -): void {
15 - let body: t.BlockStatement | t.Expression;
16 - let name: string | null = null;
17 - if (fn.type === "FunctionDeclaration") {
18 - body = fn.body;
19 - name = fn.id?.name ?? null;
20 - } else if (fn.type === "FunctionExpression") {
21 - body = fn.body;
22 - name = fn.id?.name ?? null;
23 - } else if (fn.type === "ArrowFunctionExpression") {
24 - body = fn.body;
25 - } else {
26 - assertExhaustive(fn, `Expected a function type, got '${(fn as any).type}'`);
27 - }
28 -
29 - if (name === null) {
30 - return;
31 - }
32 - if (t.isExpression(body)) {
33 - body = t.blockStatement([t.expressionStatement(body)]);
34 - }
35 -
36 - /*
37 - * Technically, this is a conditional hook call. However, we expect
38 - * __DEV__ and gatingIdentifier to be runtime constants
39 - */
40 - const test: t.IfStatement = t.ifStatement(
41 - t.identifier("__DEV__"),
42 - t.expressionStatement(
43 - t.callExpression(t.identifier(instrumentFnName), [t.stringLiteral(name)])
44 - )
45 - );
46 - body.body.unshift(test);
47 -}
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Options.ts
-22
@@ -56,27 +56,6 @@ export type PluginOptions = {
56 * var Foo = isForgetEnabled_Pokes() ? Foo_forget : Foo_uncompiled;
57 */
58 gating: ExternalFunction | null;
59 - /*
60 - * Enables instrumentation codegen. This emits a dev-mode only call to an
61 - * instrumentation function, for components and hooks that Forget compiles.
62 - * For example:
63 - * instrumentForget: {
64 - * source: 'react-forget-runtime',
65 - * importSpecifierName: 'useRenderCounter',
66 - * }
67 - *
68 - * produces:
69 - * import {useRenderCounter} from 'react-forget-runtime-pokes';
70 - *
71 - * function Component(props) {
72 - * if (__DEV__) {
73 - * useRenderCounter();
74 - * }
75 - * // ...
76 - * }
77 - *
78 - */
79 - instrumentForget: ExternalFunction | null;
59
60 panicThreshold: PanicThresholdOptions;
61
@@ -176,7 +155,6 @@ export const defaultOptions: PluginOptions = {
155 environment: {},
156 logger: null,
157 gating: null,
179 - instrumentForget: null,
158 noEmit: false,
159 enableUseMemoCachePolyfill: false,
160 } as const;
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+2 -2
@@ -39,7 +39,7 @@ import {
39 assertScopeInstructionsWithinScopes,
40 buildReactiveBlocks,
41 buildReactiveFunction,
42 - codegenReactiveFunction,
42 + codegenFunction,
43 extractScopeDeclarationsFromDestructuring,
44 flattenReactiveLoops,
45 flattenScopesWithHooks,
@@ -356,7 +356,7 @@ function* runWithEnvironment(
356 validateMemoizedEffectDependencies(reactiveFunction);
357 }
358
359 - const ast = codegenReactiveFunction(reactiveFunction).unwrap();
359 + const ast = codegenFunction(reactiveFunction).unwrap();
360 yield log({ kind: "ast", name: "Codegen", value: ast });
361
362 /**
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+4 -11
@@ -27,7 +27,6 @@ import {
27 } from "./EslintSuppression";
28 import { insertGatedFunctionDeclaration } from "./Gating";
29 import { addImportsToProgram, updateUseMemoCacheImport } from "./Imports";
30 -import { addInstrumentForget } from "./Instrumentation";
30 import { PluginOptions, parsePluginOptions } from "./Options";
31 import { compileFn } from "./Pipeline";
32
@@ -300,8 +299,10 @@ export function compileProgram(
299 externalFunctions.push(gating);
300 }
301
303 - if (options.instrumentForget != null) {
304 - instrumentForget = tryParseExternalFunction(options.instrumentForget);
302 + if (options.environment?.enableEmitInstrumentForget != null) {
303 + instrumentForget = tryParseExternalFunction(
304 + options.environment.enableEmitInstrumentForget
305 + );
306 externalFunctions.push(instrumentForget);
307 }
308
@@ -322,16 +323,8 @@ export function compileProgram(
323 */
324 for (const { originalFn, compiledFn } of compiledFns) {
325 const transformedFn = createNewFunctionNode(originalFn, compiledFn);
325 - if (instrumentForget != null) {
326 - const instrumentFnName = instrumentForget.importSpecifierName;
327 - addInstrumentForget(transformedFn, instrumentFnName);
328 - }
326
327 if (gating != null) {
331 - if (instrumentForget != null) {
332 - const instrumentFnName = instrumentForget.importSpecifierName;
333 - addInstrumentForget(originalFn.node, instrumentFnName);
334 - }
328 insertGatedFunctionDeclaration(originalFn, transformedFn, gating);
329 } else {
330 originalFn.replaceWith(transformedFn);
compiler/packages/babel-plugin-react-forget/src/Entrypoint/index.ts
-1
@@ -8,7 +8,6 @@
8 export * from "./EslintSuppression";
9 export * from "./Gating";
10 export * from "./Imports";
11 -export * from "./Instrumentation";
11 export * from "./Options";
12 export * from "./Pipeline";
13 export * from "./Program";
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+22
@@ -191,6 +191,28 @@ const EnvironmentConfigSchema = z.object({
191 */
192 enableEmitFreeze: ExternalFunctionSchema.nullish(),
193
194 + /*
195 + * Enables instrumentation codegen. This emits a dev-mode only call to an
196 + * instrumentation function, for components and hooks that Forget compiles.
197 + * For example:
198 + * instrumentForget: {
199 + * source: 'react-forget-runtime',
200 + * importSpecifierName: 'useRenderCounter',
201 + * }
202 + *
203 + * produces:
204 + * import {useRenderCounter} from 'react-forget-runtime-pokes';
205 + *
206 + * function Component(props) {
207 + * if (__DEV__) {
208 + * useRenderCounter();
209 + * }
210 + * // ...
211 + * }
212 + *
213 + */
214 + enableEmitInstrumentForget: ExternalFunctionSchema.nullish(),
215 +
216 /*
217 * Forget infers certain operations as "freezing" a value, such that those
218 * values should not be subsequently mutated. By default this freeze operation
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+30 -1
@@ -60,7 +60,36 @@ export type CodegenFunction = {
60 memoBlocks: number;
61 };
62
63 -export function codegenReactiveFunction(
63 +export function codegenFunction(
64 + fn: ReactiveFunction
65 +): Result<CodegenFunction, CompilerError> {
66 + const compileResult = codegenReactiveFunction(fn);
67 + if (compileResult.isErr()) {
68 + return compileResult;
69 + }
70 + const compiled = compileResult.unwrap();
71 +
72 + const emitInstrumentForget = fn.env.config.enableEmitInstrumentForget;
73 + if (emitInstrumentForget != null && fn.id != null) {
74 + /*
75 + * Technically, this is a conditional hook call. However, we expect
76 + * __DEV__ and gatingIdentifier to be runtime constants
77 + */
78 + const test: t.IfStatement = t.ifStatement(
79 + t.identifier("__DEV__"),
80 + t.expressionStatement(
81 + t.callExpression(
82 + t.identifier(emitInstrumentForget.importSpecifierName),
83 + [t.stringLiteral(fn.id)]
84 + )
85 + )
86 + );
87 + compiled.body.body.unshift(test);
88 + }
89 + return compileResult;
90 +}
91 +
92 +function codegenReactiveFunction(
93 fn: ReactiveFunction
94 ): Result<CodegenFunction, CompilerError> {
95 const cx = new Context(fn.env, fn.id ?? "[[ anonymous ]]");
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/index.ts
+1 -1
@@ -10,7 +10,7 @@ export { assertScopeInstructionsWithinScopes } from "./AssertScopeInstructionsWi
10 export { buildReactiveBlocks } from "./BuildReactiveBlocks";
11 export { buildReactiveFunction } from "./BuildReactiveFunction";
12 export {
13 - codegenReactiveFunction,
13 + codegenFunction,
14 type CodegenFunction,
15 } from "./CodegenReactiveFunction";
16 export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclarationsFromDestructuring";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/codegen-instrument-forget-gating-test.expect.md
-2
@@ -42,7 +42,6 @@ const Bar = isForgetEnabled_Fixtures()
42 }
43 : function Bar(props) {
44 "use forget";
45 - if (__DEV__) useRenderCounter("Bar");
45 return <div>{props.bar}</div>;
46 };
47
@@ -65,7 +64,6 @@ const Foo = isForgetEnabled_Fixtures()
64 }
65 : function Foo(props) {
66 "use forget";
68 - if (__DEV__) useRenderCounter("Foo");
67 return <Foo>{props.bar}</Foo>;
68 };
69
compiler/packages/fixture-test-utils/src/compiler-utils.ts
+3 -3
@@ -31,7 +31,7 @@ export function transformFixtureInput(
31
32 let language = parseLanguage(firstLine);
33 let gating = null;
34 - let instrumentForget = null;
34 + let enableEmitInstrumentForget = null;
35 let enableEmitFreeze = null;
36 let compilationMode: CompilationMode = "all";
37 let enableUseMemoCachePolyfill = false;
@@ -59,7 +59,7 @@ export function transformFixtureInput(
59 };
60 }
61 if (firstLine.includes("@instrumentForget")) {
62 - instrumentForget = {
62 + enableEmitInstrumentForget = {
63 source: "react-forget-runtime",
64 importSpecifierName: "useRenderCounter",
65 };
@@ -115,12 +115,12 @@ export function transformFixtureInput(
115 ],
116 ]),
117 enableEmitFreeze,
118 + enableEmitInstrumentForget,
119 assertValidMutableRanges: true,
120 },
121 compilationMode,
122 logger: null,
123 gating,
123 - instrumentForget,
124 panicThreshold,
125 noEmit: false,
126 enableUseMemoCachePolyfill,