From 90e189ae82022d5183f1711d44cc2d7ee02af1ab Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 17 Oct 2022 13:08:34 -0400 Subject: [PATCH] Add react-forget-runtime This is a new module that holds: - the `useMemoCache` stub (hopefully to be deleted next week) - various helpers that can be imported by the compiler, e.g. the dispatcher guard `$startLazy` - skipped the implementation of `makeReadOnly` for now as there's already multiple copies and I wanted to avoid typescript in this file for now to make the build easier (i.e. no build) --- .../packages/react-forget-runtime/index.js | 86 +++++++++++++++++++ .../react-forget-runtime/package.json | 10 +++ compiler/forget/scripts/jest/makeTransform.js | 23 ++++- compiler/forget/src/BackEnd/JS.ts | 30 ++----- compiler/forget/src/BackEnd/JSGen.ts | 24 ++++++ .../forget/src/__tests__/e2e/hello.e2e.js | 4 +- .../src/__tests__/e2e/update-button.e2e.js | 6 +- .../__tests__/test-utils/useMemoCacheStub.ts | 26 ------ 8 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 compiler/forget/packages/react-forget-runtime/index.js create mode 100644 compiler/forget/packages/react-forget-runtime/package.json delete mode 100644 compiler/forget/src/__tests__/test-utils/useMemoCacheStub.ts diff --git a/compiler/forget/packages/react-forget-runtime/index.js b/compiler/forget/packages/react-forget-runtime/index.js new file mode 100644 index 0000000000..3f0feafdc8 --- /dev/null +++ b/compiler/forget/packages/react-forget-runtime/index.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as React from "react"; + +const { + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { + ReactCurrentDispatcher, + }, + useRef, +} = React; + +export const $empty = Symbol.for("react.usememocache_sentinel"); + +export function unstable_useMemoCache(i) { + "use no forget"; + const $ = new Array(i).fill($empty); + return useRef($).current; +} + +export function $read(memoCache, index) { + const value = memoCache[index]; + if (value === $empty) { + throw new Error("useMemoCache: read before write"); + } + return value; +} + +const LazyGuardDispatcher = {}; +[ + "readContext", + "useCallback", + "useContext", + "useEffect", + "useImperativeHandle", + "useInsertionEffect", + "useLayoutEffect", + "useMemo", + "useReducer", + "useRef", + "useState", + "useDebugValue", + "useDeferredValue", + "useTransition", + "useMutableSource", + "useSyncExternalStore", + "useId", + "unstable_isNewReconciler", + "getCacheSignal", + "getCacheForType", + "useCacheRefresh", +].forEach((name) => { + LazyGuardDispatcher[name] = () => { + throw new Error(`Cannot call ${name} within ReactForget lazy block.`); + }; +}); + +let originalDispatcher = null; + +export function $startLazy() { + if (originalDispatcher !== null) { + throw new Error("unexpected startLazy with dispatcher set"); + } + originalDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = LazyGuardDispatcher; +} + +export function $endLazy() { + if (originalDispatcher === null) { + throw new Error("unexpected endLazy with dispatcher not set"); + } + ReactCurrentDispatcher.current = originalDispatcher; + originalDispatcher = null; +} + +export function $reset($) { + $.fill($empty); +} + +export function $makeReadOnly() { + throw new Error("TODO: implement $makeReadOnly in react-forget-runtime"); +} diff --git a/compiler/forget/packages/react-forget-runtime/package.json b/compiler/forget/packages/react-forget-runtime/package.json new file mode 100644 index 0000000000..34587052ce --- /dev/null +++ b/compiler/forget/packages/react-forget-runtime/package.json @@ -0,0 +1,10 @@ +{ + "name": "react-forget-runtime", + "version": "0.0.1", + "description": "Runtime for React Forget", + "main": "index.js", + "license": "MIT", + "files": [ + "index.js" + ] +} diff --git a/compiler/forget/scripts/jest/makeTransform.js b/compiler/forget/scripts/jest/makeTransform.js index 8fe8112ae1..128d54ff47 100644 --- a/compiler/forget/scripts/jest/makeTransform.js +++ b/compiler/forget/scripts/jest/makeTransform.js @@ -33,7 +33,28 @@ module.exports = (useForget) => { }, "@babel/preset-react", { - plugins: ["@babel/plugin-transform-modules-commonjs"], + plugins: [ + [ + function BabelPluginRewriteRequirePath(babel) { + return { + visitor: { + CallExpression(path) { + if (path.node.callee.name === "require") { + const arg = path.node.arguments[0]; + if (arg.type === "StringLiteral") { + if (arg.value === "react-forget-runtime") { + arg.value = + "../../../packages/react-forget-runtime"; + } + } + } + }, + }, + }; + }, + ], + "@babel/plugin-transform-modules-commonjs", + ], }, ], targets: { diff --git a/compiler/forget/src/BackEnd/JS.ts b/compiler/forget/src/BackEnd/JS.ts index d8c800077e..b5eef97090 100644 --- a/compiler/forget/src/BackEnd/JS.ts +++ b/compiler/forget/src/BackEnd/JS.ts @@ -100,13 +100,13 @@ class MemoCache { } /** - * `$[entry] == $._` + * `$[entry] == $empty` */ isEmpty(entry: LIR.MemoCache.Entry): t.BinaryExpression { return t.binaryExpression( "===", this.#cacheMember(entry.index), - t.memberExpression(this.id, t.identifier("_")) + t.identifier("$empty") ); } @@ -122,10 +122,10 @@ class MemoCache { #readIndex(index: number): t.Expression { if (this.#guardReads) { - return t.callExpression( - t.memberExpression(t.identifier("useMemoCache"), t.identifier("read")), - [this.id, t.numericLiteral(index)] - ); + return t.callExpression(t.identifier("$read"), [ + this.id, + t.numericLiteral(index), + ]); } else { return this.#cacheMember(index); } @@ -193,7 +193,7 @@ export class Func { if (this.context.opts.flags.addFreeze === true) { this.code.push( t.expressionStatement( - t.callExpression(t.identifier("useMemoCache.makeReadOnly"), [ + t.callExpression(t.identifier("$makeReadOnly"), [ val.binding.identifier, ]) ) @@ -382,22 +382,10 @@ export class Func { if (this.context.opts.flags.guardHooks) { const startLazyCall = t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier("useMemoCache"), - t.identifier("startLazy") - ), - [] - ) + t.callExpression(t.identifier("$startLazy"), []) ); const endLazyCall = t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier("useMemoCache"), - t.identifier("endLazy") - ), - [] - ) + t.callExpression(t.identifier("$endLazy"), []) ); evalBody = [ t.tryStatement( diff --git a/compiler/forget/src/BackEnd/JSGen.ts b/compiler/forget/src/BackEnd/JSGen.ts index eb59965327..7d57ccb87f 100644 --- a/compiler/forget/src/BackEnd/JSGen.ts +++ b/compiler/forget/src/BackEnd/JSGen.ts @@ -42,6 +42,30 @@ export function run(lirProg: LIR.Prog, context: CompilerContext) { /*source*/ t.stringLiteral("react") ) ); + const utils = ["$empty"]; + if (context.opts.flags.guardHooks) { + utils.push("$startLazy", "$endLazy"); + } + if (context.opts.flags.guardReads) { + utils.push("$read"); + } + if (context.opts.flags.addFreeze) { + utils.push("$makeReadOnly"); + } + if (context.opts.flags.guardThrows) { + utils.push("$reset"); + } + if (utils.length > 0) { + prog.unshiftContainer( + "body", + t.importDeclaration( + utils.map((name) => + t.importSpecifier(t.identifier(name), t.identifier(name)) + ), + t.stringLiteral("react-forget-runtime") + ) + ); + } } for (const [irFunc, lirFunc] of lirProg.funcs) { diff --git a/compiler/forget/src/__tests__/e2e/hello.e2e.js b/compiler/forget/src/__tests__/e2e/hello.e2e.js index ed76812cdb..add02808f6 100644 --- a/compiler/forget/src/__tests__/e2e/hello.e2e.js +++ b/compiler/forget/src/__tests__/e2e/hello.e2e.js @@ -5,12 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import { useMemoCacheStub } from "../test-utils/useMemoCacheStub"; import * as React from "react"; import { render } from "@testing-library/react"; import { expectLogsAndClear, log } from "./expectLogs"; -React.unstable_useMemoCache = useMemoCacheStub; +import { unstable_useMemoCache } from "react-forget-runtime"; +React.unstable_useMemoCache = unstable_useMemoCache; function Hello({ name }) { "use forget"; diff --git a/compiler/forget/src/__tests__/e2e/update-button.e2e.js b/compiler/forget/src/__tests__/e2e/update-button.e2e.js index 11cae8dc82..37bcf74783 100644 --- a/compiler/forget/src/__tests__/e2e/update-button.e2e.js +++ b/compiler/forget/src/__tests__/e2e/update-button.e2e.js @@ -5,11 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import { render } from "@testing-library/react"; import * as React from "react"; -import { useMemoCacheStub } from "../test-utils/useMemoCacheStub"; +import { render } from "@testing-library/react"; -React.unstable_useMemoCache = useMemoCacheStub; +import { unstable_useMemoCache } from "react-forget-runtime"; +React.unstable_useMemoCache = unstable_useMemoCache; function Button({ label }) { const theme = useTheme(); diff --git a/compiler/forget/src/__tests__/test-utils/useMemoCacheStub.ts b/compiler/forget/src/__tests__/test-utils/useMemoCacheStub.ts deleted file mode 100644 index 2c00cf96d0..0000000000 --- a/compiler/forget/src/__tests__/test-utils/useMemoCacheStub.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { useRef } from "react"; - -const REACT_MEMOCACHE_SENTINEL = Symbol.for("react.memocache_sentinel"); - -/** - * Stub for `React.useMemoCache` used in E2E tests until we have a React - * version that supports it. - * - * @param i Cache size - * @returns Memo cache - */ -export function useMemoCacheStub(i: number) { - "use no forget"; - const $ = new Array(i).fill(REACT_MEMOCACHE_SENTINEL); - // @ts-ignore - $._ = REACT_MEMOCACHE_SENTINEL; - const ref = useRef($); - return ref.current; -}