diff --git a/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts index 08c4f5cbf0..3025709be2 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts @@ -63,6 +63,7 @@ import { pruneUnusedScopes, renameVariables, } from "../ReactiveScopes"; +import { pruneAlwaysInvalidatingScopes } from "../ReactiveScopes/PruneAlwaysInvalidatingScopes"; import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA"; import { inferTypes } from "../TypeInference"; import { @@ -319,6 +320,13 @@ function* runWithEnvironment( value: reactiveFunction, }); + pruneAlwaysInvalidatingScopes(reactiveFunction); + yield log({ + kind: "reactive", + name: "PruneAlwaysInvalidatingScopes", + value: reactiveFunction, + }); + promoteUsedTemporaries(reactiveFunction); yield log({ kind: "reactive", diff --git a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts new file mode 100644 index 0000000000..d83df61345 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneAlwaysInvalidatingScopes.ts @@ -0,0 +1,115 @@ +/* + * 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 { + ReactiveFunctionTransform, + Transformed, + visitReactiveFunction, +} from "."; +import { + IdentifierId, + ReactiveFunction, + ReactiveInstruction, + ReactiveScopeBlock, + ReactiveStatement, +} from "../HIR"; + +/** + * Some instructions will *always* produce a new value, and unless memoized will *always* + * invalidate downstream reactive scopes. This pass finds such values and prunes downstream + * memoization. + * + * NOTE: function calls are an edge-case: function calls *may* return primitives, so this + * pass optimistically assumes they do. Therefore, unmemoized function calls will *not* + * prune downstream memoization. Only guaranteed new allocations, such as object and array + * literals, will cause pruning. + */ +export function pruneAlwaysInvalidatingScopes(fn: ReactiveFunction): void { + visitReactiveFunction(fn, new Transform(), false); +} + +class Transform extends ReactiveFunctionTransform { + alwaysInvalidatingValues: Set = new Set(); + unmemoizedValues: Set = new Set(); + + override transformInstruction( + instruction: ReactiveInstruction, + withinScope: boolean + ): Transformed { + this.visitInstruction(instruction, withinScope); + + const { lvalue, value } = instruction; + switch (value.kind) { + case "ArrayExpression": + case "ObjectExpression": + case "JsxExpression": + case "JsxFragment": + case "NewExpression": { + if (lvalue !== null) { + this.alwaysInvalidatingValues.add(lvalue.identifier.id); + if (!withinScope) { + this.unmemoizedValues.add(lvalue.identifier.id); + } + } + break; + } + case "StoreLocal": { + if (this.alwaysInvalidatingValues.has(value.value.identifier.id)) { + this.alwaysInvalidatingValues.add(value.lvalue.place.identifier.id); + } + if (this.unmemoizedValues.has(value.value.identifier.id)) { + this.unmemoizedValues.add(value.lvalue.place.identifier.id); + } + break; + } + case "LoadLocal": { + if ( + lvalue !== null && + this.alwaysInvalidatingValues.has(value.place.identifier.id) + ) { + this.alwaysInvalidatingValues.add(lvalue.identifier.id); + } + if ( + lvalue !== null && + this.unmemoizedValues.has(value.place.identifier.id) + ) { + this.unmemoizedValues.add(lvalue.identifier.id); + } + break; + } + } + return { kind: "keep" }; + } + + override transformScope( + scopeBlock: ReactiveScopeBlock, + _withinScope: boolean + ): Transformed { + this.visitScope(scopeBlock, true); + + for (const dep of scopeBlock.scope.dependencies) { + if (this.unmemoizedValues.has(dep.identifier.id)) { + /* + * This scope depends on an always-invalidating value so the scope will always invalidate: + * prune it to avoid wasted comparisons + */ + for (const [id, _decl] of scopeBlock.scope.declarations) { + if (this.alwaysInvalidatingValues.has(id)) { + this.unmemoizedValues.add(id); + } + } + for (const identifier of scopeBlock.scope.reassignments) { + if (this.alwaysInvalidatingValues.has(identifier.id)) { + this.unmemoizedValues.add(identifier.id); + } + } + return { kind: "replace-many", value: scopeBlock.instructions }; + } + } + return { kind: "keep" }; + } +} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-array.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-array.expect.md index 9b74851e98..00c59b2450 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-array.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-array.expect.md @@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { unstable_useMemoCache as useMemoCache } from "react"; import { useHook } from "shared-runtime"; function Component(props) { - const $ = useMemoCache(4); const x = []; useHook(); x.push(props.value); - let t0; - if ($[0] !== x) { - t0 = [x]; - $[0] = x; - $[1] = t0; - } else { - t0 = $[1]; - } - const y = t0; - let t1; - if ($[2] !== y) { - t1 = [y]; - $[2] = y; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + + const y = [x]; + return [y]; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md index 3cbff1f94f..6dc42be6b9 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-new.expect.md @@ -26,32 +26,15 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { unstable_useMemoCache as useMemoCache } from "react"; import { useHook } from "shared-runtime"; function Component(props) { - const $ = useMemoCache(4); const x = new Foo(); useHook(); x.value = props.value; - let t0; - if ($[0] !== x) { - t0 = { x }; - $[0] = x; - $[1] = t0; - } else { - t0 = $[1]; - } - const y = t0; - let t1; - if ($[2] !== y) { - t1 = { y }; - $[2] = y; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + + const y = { x }; + return { y }; } class Foo {} diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md index 5b87500316..a5b86367c5 100644 --- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/prune-scopes-whose-deps-invalidate-object.expect.md @@ -24,32 +24,15 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { unstable_useMemoCache as useMemoCache } from "react"; import { useHook } from "shared-runtime"; function Component(props) { - const $ = useMemoCache(4); const x = {}; useHook(); x.value = props.value; - let t0; - if ($[0] !== x) { - t0 = { x }; - $[0] = x; - $[1] = t0; - } else { - t0 = $[1]; - } - const y = t0; - let t1; - if ($[2] !== y) { - t1 = { y }; - $[2] = y; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; + + const y = { x }; + return { y }; } export const FIXTURE_ENTRYPOINT = {