[devx] emit calls to `makeReadOnly` for debugging
--- Emit calls to makeReadOnly for memoized values. ```js function MyComponent() { let x; if (c_0) { x = // ... (recompute x) $[0] = __DEV__ ? makeReadOnly(x, "MyComponent") : x; } else { x = $[0] } } ``` - import source / specifier should be configurable, as - we'll likely want to add gk gating to `makeReadOnly` itself to reduce codesize bloat - each Forget project needs different logging and filter configurations - codegen function name as an argument for easier debugging - only freeze memoized outputs
Mofei Zhang committed
Jun 5, 2023 at 14:46 UTC
a65e9cd1f13aed33c12c5ea20ee21cacd414ac22
6 files changed
+134
-5
compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts
+7
-1
@@ -12,10 +12,10 @@ import {
12
CompilerErrorDetail,
13
ErrorSeverity,
14
} from "../CompilerError";
15
-import { compileFn } from "./Pipeline";
15
import { GeneratedSource } from "../HIR";
16
import { addInstrumentForget } from "./Instrumentation";
17
import { ExternalFunction, PluginOptions, parsePluginOptions } from "./Options";
18
+import { compileFn } from "./Pipeline";
19
20
export type CompilerPass = {
21
opts: PluginOptions;
@@ -332,6 +332,12 @@ export function compileProgram(
332
buildImportForExternalFunction(options.instrumentForget.instrumentFn)
333
);
334
}
335
+ if (options.environment?.enableEmitFreeze != null) {
336
+ program.unshiftContainer(
337
+ "body",
338
+ buildImportForExternalFunction(options.environment?.enableEmitFreeze)
339
+ );
340
+ }
341
}
342
}
343
compiler/forget/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+28
@@ -33,6 +33,7 @@ import {
33
ShapeRegistry,
34
addHook,
35
} from "./ObjectShape";
36
+import { ExternalFunction } from "../Entrypoint/Options";
37
38
export type Hook = {
39
effectKind: Effect;
@@ -126,6 +127,29 @@ export type EnvironmentConfig = Partial<{
127
* Defaults to false (ie, by default memoization is enabled)
128
*/
129
disableAllMemoization: boolean;
130
+
131
+ /**
132
+ * Enables codegen mutability debugging. This emits a dev-mode only to log mutations
133
+ * to values that Forget assumes are immutable (for Forget compiled code).
134
+ * For example:
135
+ * emitFreeze: {
136
+ * source: 'ReactForgetRuntime',
137
+ * importSpecifierName: 'makeReadOnly',
138
+ * }
139
+ *
140
+ * produces:
141
+ * import {makeReadOnly} from 'ReactForgetRuntime';
142
+ *
143
+ * function Component(props) {
144
+ * if (c_0) {
145
+ * // ...
146
+ * $[0] = __DEV__ ? makeReadOnly(x) : x;
147
+ * } else {
148
+ * x = $[0];
149
+ * }
150
+ * }
151
+ */
152
+ enableEmitFreeze: ExternalFunction | null;
153
}>;
154
155
export class Environment {
@@ -140,6 +164,8 @@ export class Environment {
164
enableAssumeHooksFollowRulesOfReact: boolean;
165
enableTreatHooksAsFunctions: boolean;
166
disableAllMemoization: boolean;
167
+ enableEmitFreeze: ExternalFunction | null;
168
+
169
#contextIdentifiers: Set<t.Identifier>;
170
171
constructor(
@@ -181,6 +207,8 @@ export class Environment {
207
this.enableTreatHooksAsFunctions =
208
config?.enableTreatHooksAsFunctions ?? true;
209
this.disableAllMemoization = config?.disableAllMemoization ?? false;
210
+ this.enableEmitFreeze = config?.enableEmitFreeze ?? null;
211
+
212
this.#contextIdentifiers = contextIdentifiers;
213
}
214
compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+26
-4
@@ -8,6 +8,7 @@
8
import * as t from "@babel/types";
9
import invariant from "invariant";
10
import { CompilerError, ErrorSeverity } from "../CompilerError";
11
+import { Environment } from "../HIR";
12
import {
13
BlockId,
14
GeneratedSource,
@@ -35,7 +36,7 @@ import { assertExhaustive } from "../Utils/utils";
36
export function codegenReactiveFunction(
37
fn: ReactiveFunction
38
): Result<t.FunctionDeclaration, CompilerError> {
38
- const cx = new Context();
39
+ const cx = new Context(fn.env, fn.id?.name ?? "[[ anonymous ]]");
40
if (fn.id !== null) {
41
cx.temp.set(fn.id.id, null);
42
}
@@ -84,11 +85,16 @@ export function codegenReactiveFunction(
85
}
86
87
class Context {
88
+ env: Environment;
89
+ fnName: string;
90
#nextCacheIndex: number = 0;
91
#declarations: Set<IdentifierId> = new Set();
92
temp: Temporaries = new Map();
93
errors: CompilerError = new CompilerError();
91
-
94
+ constructor(env: Environment, fnName: string) {
95
+ this.env = env;
96
+ this.fnName = fnName;
97
+ }
98
get nextCacheIndex(): number {
99
return this.#nextCacheIndex++;
100
}
@@ -147,6 +153,22 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
153
return t.blockStatement(statements);
154
}
155
156
+function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
157
+ if (cx.env.enableEmitFreeze != null) {
158
+ // The import declaration for emitFreeze is inserted in the Babel plugin
159
+ return t.conditionalExpression(
160
+ t.identifier("__DEV__"),
161
+ t.callExpression(
162
+ t.identifier(cx.env.enableEmitFreeze.importSpecifierName),
163
+ [value, t.stringLiteral(cx.fnName)]
164
+ ),
165
+ value
166
+ );
167
+ } else {
168
+ return value;
169
+ }
170
+}
171
+
172
function codegenReactiveScope(
173
cx: Context,
174
statements: Array<t.Statement>,
@@ -212,7 +234,7 @@ function codegenReactiveScope(
234
t.assignmentExpression(
235
"=",
236
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
215
- name
237
+ wrapCacheDep(cx, name)
238
)
239
)
240
);
@@ -239,7 +261,7 @@ function codegenReactiveScope(
261
t.assignmentExpression(
262
"=",
263
t.memberExpression(t.identifier("$"), t.numericLiteral(index), true),
242
- name
264
+ wrapCacheDep(cx, name)
265
)
266
)
267
);
compiler/forget/packages/snap/src/compiler-worker.ts
+8
@@ -97,6 +97,7 @@ export async function compile(
97
let enableTreatHooksAsFunctions = true;
98
let disableAllMemoization = false;
99
let validateRefAccessDuringRender = true;
100
+ let enableEmitFreeze = null;
101
if (firstLine.indexOf("@forgetDirective") !== -1) {
102
enableOnlyOnUseForgetDirective = true;
103
}
@@ -136,6 +137,12 @@ export async function compile(
137
if (firstLine.indexOf("@validateRefAccessDuringRender false") !== -1) {
138
validateRefAccessDuringRender = false;
139
}
140
+ if (firstLine.indexOf("@enableEmitFreeze") !== -1) {
141
+ enableEmitFreeze = {
142
+ source: "react-forget-runtime-emit-freeze",
143
+ importSpecifierName: "makeReadOnly",
144
+ };
145
+ }
146
147
const language = parseLanguage(firstLine);
148
@@ -160,6 +167,7 @@ export async function compile(
167
validateHooksUsage: true,
168
validateRefAccessDuringRender,
169
validateFrozenLambdas: true,
170
+ enableEmitFreeze,
171
},
172
logger: null,
173
gating,
compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.expect.md
new
+54
@@ -0,0 +1,54 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableEmitFreeze true
6
+
7
+function MyComponentName(props) {
8
+ let x = {};
9
+ foo(x, props.a);
10
+ foo(x, props.b);
11
+
12
+ let y = [];
13
+ y.push(x);
14
+ return y;
15
+}
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { makeReadOnly } from "react-forget-runtime-emit-freeze";
23
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEmitFreeze true
24
+
25
+function MyComponentName(props) {
26
+ const $ = useMemoCache(5);
27
+ const c_0 = $[0] !== props.a;
28
+ const c_1 = $[1] !== props.b;
29
+ let x;
30
+ if (c_0 || c_1) {
31
+ x = {};
32
+ foo(x, props.a);
33
+ foo(x, props.b);
34
+ $[0] = props.a;
35
+ $[1] = props.b;
36
+ $[2] = __DEV__ ? makeReadOnly(x, "MyComponentName") : x;
37
+ } else {
38
+ x = $[2];
39
+ }
40
+ const c_3 = $[3] !== x;
41
+ let y;
42
+ if (c_3) {
43
+ y = [];
44
+ y.push(x);
45
+ $[3] = x;
46
+ $[4] = __DEV__ ? makeReadOnly(y, "MyComponentName") : y;
47
+ } else {
48
+ y = $[4];
49
+ }
50
+ return y;
51
+}
52
+
53
+```
54
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.js
new
+11
@@ -0,0 +1,11 @@
1
+// @enableEmitFreeze true
2
+
3
+function MyComponentName(props) {
4
+ let x = {};
5
+ foo(x, props.a);
6
+ foo(x, props.b);
7
+
8
+ let y = [];
9
+ y.push(x);
10
+ return y;
11
+}