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