@samitouri / QOS-React-2 / commits / a65e9cd1f1

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