From a65e9cd1f13aed33c12c5ea20ee21cacd414ac22 Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Mon, 5 Jun 2023 14:46:58 -0400 Subject: [PATCH] [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 --- .../src/Entrypoint/Program.ts | 8 ++- .../src/HIR/Environment.ts | 28 ++++++++++ .../ReactiveScopes/CodegenReactiveFunction.ts | 30 +++++++++-- .../packages/snap/src/compiler-worker.ts | 8 +++ .../compiler/emit-make-read-only.expect.md | 54 +++++++++++++++++++ .../fixtures/compiler/emit-make-read-only.js | 11 ++++ 6 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.js diff --git a/compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts b/compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts index 1de6886ce3..fa5b46a62f 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts +++ b/compiler/forget/packages/babel-plugin-react-forget/src/Entrypoint/Program.ts @@ -12,10 +12,10 @@ import { CompilerErrorDetail, ErrorSeverity, } from "../CompilerError"; -import { compileFn } from "./Pipeline"; import { GeneratedSource } from "../HIR"; import { addInstrumentForget } from "./Instrumentation"; import { ExternalFunction, PluginOptions, parsePluginOptions } from "./Options"; +import { compileFn } from "./Pipeline"; export type CompilerPass = { opts: PluginOptions; @@ -332,6 +332,12 @@ export function compileProgram( buildImportForExternalFunction(options.instrumentForget.instrumentFn) ); } + if (options.environment?.enableEmitFreeze != null) { + program.unshiftContainer( + "body", + buildImportForExternalFunction(options.environment?.enableEmitFreeze) + ); + } } } diff --git a/compiler/forget/packages/babel-plugin-react-forget/src/HIR/Environment.ts b/compiler/forget/packages/babel-plugin-react-forget/src/HIR/Environment.ts index dda2d6e123..3960292b30 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/src/HIR/Environment.ts +++ b/compiler/forget/packages/babel-plugin-react-forget/src/HIR/Environment.ts @@ -33,6 +33,7 @@ import { ShapeRegistry, addHook, } from "./ObjectShape"; +import { ExternalFunction } from "../Entrypoint/Options"; export type Hook = { effectKind: Effect; @@ -126,6 +127,29 @@ export type EnvironmentConfig = Partial<{ * Defaults to false (ie, by default memoization is enabled) */ disableAllMemoization: boolean; + + /** + * Enables codegen mutability debugging. This emits a dev-mode only to log mutations + * to values that Forget assumes are immutable (for Forget compiled code). + * For example: + * emitFreeze: { + * source: 'ReactForgetRuntime', + * importSpecifierName: 'makeReadOnly', + * } + * + * produces: + * import {makeReadOnly} from 'ReactForgetRuntime'; + * + * function Component(props) { + * if (c_0) { + * // ... + * $[0] = __DEV__ ? makeReadOnly(x) : x; + * } else { + * x = $[0]; + * } + * } + */ + enableEmitFreeze: ExternalFunction | null; }>; export class Environment { @@ -140,6 +164,8 @@ export class Environment { enableAssumeHooksFollowRulesOfReact: boolean; enableTreatHooksAsFunctions: boolean; disableAllMemoization: boolean; + enableEmitFreeze: ExternalFunction | null; + #contextIdentifiers: Set; constructor( @@ -181,6 +207,8 @@ export class Environment { this.enableTreatHooksAsFunctions = config?.enableTreatHooksAsFunctions ?? true; this.disableAllMemoization = config?.disableAllMemoization ?? false; + this.enableEmitFreeze = config?.enableEmitFreeze ?? null; + this.#contextIdentifiers = contextIdentifiers; } diff --git a/compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts index 0e4e6f5501..7578866932 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/forget/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -8,6 +8,7 @@ import * as t from "@babel/types"; import invariant from "invariant"; import { CompilerError, ErrorSeverity } from "../CompilerError"; +import { Environment } from "../HIR"; import { BlockId, GeneratedSource, @@ -35,7 +36,7 @@ import { assertExhaustive } from "../Utils/utils"; export function codegenReactiveFunction( fn: ReactiveFunction ): Result { - const cx = new Context(); + const cx = new Context(fn.env, fn.id?.name ?? "[[ anonymous ]]"); if (fn.id !== null) { cx.temp.set(fn.id.id, null); } @@ -84,11 +85,16 @@ export function codegenReactiveFunction( } class Context { + env: Environment; + fnName: string; #nextCacheIndex: number = 0; #declarations: Set = new Set(); temp: Temporaries = new Map(); errors: CompilerError = new CompilerError(); - + constructor(env: Environment, fnName: string) { + this.env = env; + this.fnName = fnName; + } get nextCacheIndex(): number { return this.#nextCacheIndex++; } @@ -147,6 +153,22 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement { return t.blockStatement(statements); } +function wrapCacheDep(cx: Context, value: t.Expression): t.Expression { + if (cx.env.enableEmitFreeze != null) { + // The import declaration for emitFreeze is inserted in the Babel plugin + return t.conditionalExpression( + t.identifier("__DEV__"), + t.callExpression( + t.identifier(cx.env.enableEmitFreeze.importSpecifierName), + [value, t.stringLiteral(cx.fnName)] + ), + value + ); + } else { + return value; + } +} + function codegenReactiveScope( cx: Context, statements: Array, @@ -212,7 +234,7 @@ function codegenReactiveScope( t.assignmentExpression( "=", t.memberExpression(t.identifier("$"), t.numericLiteral(index), true), - name + wrapCacheDep(cx, name) ) ) ); @@ -239,7 +261,7 @@ function codegenReactiveScope( t.assignmentExpression( "=", t.memberExpression(t.identifier("$"), t.numericLiteral(index), true), - name + wrapCacheDep(cx, name) ) ) ); diff --git a/compiler/forget/packages/snap/src/compiler-worker.ts b/compiler/forget/packages/snap/src/compiler-worker.ts index d3d3d07dbf..5028506beb 100644 --- a/compiler/forget/packages/snap/src/compiler-worker.ts +++ b/compiler/forget/packages/snap/src/compiler-worker.ts @@ -97,6 +97,7 @@ export async function compile( let enableTreatHooksAsFunctions = true; let disableAllMemoization = false; let validateRefAccessDuringRender = true; + let enableEmitFreeze = null; if (firstLine.indexOf("@forgetDirective") !== -1) { enableOnlyOnUseForgetDirective = true; } @@ -136,6 +137,12 @@ export async function compile( if (firstLine.indexOf("@validateRefAccessDuringRender false") !== -1) { validateRefAccessDuringRender = false; } + if (firstLine.indexOf("@enableEmitFreeze") !== -1) { + enableEmitFreeze = { + source: "react-forget-runtime-emit-freeze", + importSpecifierName: "makeReadOnly", + }; + } const language = parseLanguage(firstLine); @@ -160,6 +167,7 @@ export async function compile( validateHooksUsage: true, validateRefAccessDuringRender, validateFrozenLambdas: true, + enableEmitFreeze, }, logger: null, gating, diff --git a/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.expect.md new file mode 100644 index 0000000000..49473d8044 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableEmitFreeze true + +function MyComponentName(props) { + let x = {}; + foo(x, props.a); + foo(x, props.b); + + let y = []; + y.push(x); + return y; +} + +``` + +## Code + +```javascript +import { makeReadOnly } from "react-forget-runtime-emit-freeze"; +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEmitFreeze true + +function MyComponentName(props) { + const $ = useMemoCache(5); + const c_0 = $[0] !== props.a; + const c_1 = $[1] !== props.b; + let x; + if (c_0 || c_1) { + x = {}; + foo(x, props.a); + foo(x, props.b); + $[0] = props.a; + $[1] = props.b; + $[2] = __DEV__ ? makeReadOnly(x, "MyComponentName") : x; + } else { + x = $[2]; + } + const c_3 = $[3] !== x; + let y; + if (c_3) { + y = []; + y.push(x); + $[3] = x; + $[4] = __DEV__ ? makeReadOnly(y, "MyComponentName") : y; + } else { + y = $[4]; + } + return y; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.js b/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.js new file mode 100644 index 0000000000..8ad3e859e6 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/emit-make-read-only.js @@ -0,0 +1,11 @@ +// @enableEmitFreeze true + +function MyComponentName(props) { + let x = {}; + foo(x, props.a); + foo(x, props.b); + + let y = []; + y.push(x); + return y; +}