[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
This commit is contained in:
Mofei Zhang
2023-06-05 14:46:58 -04:00
parent 40d1459115
commit a65e9cd1f1
6 changed files with 134 additions and 5 deletions
@@ -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)
);
}
}
}
@@ -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<t.Identifier>;
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;
}
@@ -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<t.FunctionDeclaration, CompilerError> {
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<IdentifierId> = 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<t.Statement>,
@@ -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)
)
)
);
@@ -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,
@@ -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;
}
```
@@ -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;
}