@samitouri / QOS-React-2 / commits / 12de013aa4

[patch] ObjectMethods should have the same scope as their parent ObjectExpressions

[patch] ObjectMethods should have the same scope as their parent ObjectExpressions --- Currently, we're removing all reactive scopes containing object methods. This could produce incorrect output as object method instructions may still be included in other reactive scopes (and will lose their dependencies).

Mofei Zhang committed Dec 12, 2023 at 17:44 UTC 12de013aa4b75dbe1251f816bcb16d0a2869f7bd
16 files changed +256 -148
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+8 -8
@@ -35,6 +35,7 @@ import {
35 } from "../Optimization";
36 import {
37 CodegenFunction,
38 + alignObjectMethodScopes,
39 alignReactiveScopesToBlockScopes,
40 assertScopeInstructionsWithinScopes,
41 buildReactiveBlocks,
@@ -43,7 +44,6 @@ import {
44 extractScopeDeclarationsFromDestructuring,
45 flattenReactiveLoops,
46 flattenScopesWithHooks,
46 - flattenScopesWithObjectMethods,
47 inferReactiveScopeVariables,
48 memoizeFbtOperandsInSameScope,
49 mergeOverlappingReactiveScopes,
@@ -198,6 +198,13 @@ function* runWithEnvironment(
198 inferReactiveScopeVariables(hir);
199 yield log({ kind: "hir", name: "InferReactiveScopeVariables", value: hir });
200
201 + alignObjectMethodScopes(hir);
202 + yield log({
203 + kind: "hir",
204 + name: "AlignObjectMethodScopes",
205 + value: hir,
206 + });
207 +
208 const reactiveFunction = buildReactiveFunction(hir);
209 yield log({
210 kind: "reactive",
@@ -265,13 +272,6 @@ function* runWithEnvironment(
272 value: reactiveFunction,
273 });
274
268 - flattenScopesWithObjectMethods(reactiveFunction);
269 - yield log({
270 - kind: "reactive",
271 - name: "FlattenScopesWithObjectMethods",
272 - value: reactiveFunction,
273 - });
274 -
275 propagateScopeDependencies(reactiveFunction);
276 yield log({
277 kind: "reactive",
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignObjectMethodScopes.ts new
+101
@@ -0,0 +1,101 @@
1 +/*
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { CompilerError } from "..";
9 +import {
10 + GeneratedSource,
11 + HIRFunction,
12 + Identifier,
13 + ReactiveScope,
14 + makeInstructionId,
15 +} from "../HIR";
16 +import { eachInstructionValueOperand } from "../HIR/visitors";
17 +import DisjointSet from "../Utils/DisjointSet";
18 +
19 +/**
20 + * Align scopes of object method values to that of their enclosing object expressions.
21 + * To produce a well-formed JS program in Codegen, object methods and object expressions
22 + * must be in the same ReactiveBlock as object method definitions must be inlined.
23 + */
24 +
25 +function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> {
26 + const objectMethodDecls: Set<Identifier> = new Set();
27 + const mergeScopesBuilder = new DisjointSet<ReactiveScope>();
28 +
29 + for (const [_, block] of fn.body.blocks) {
30 + for (const { lvalue, value } of block.instructions) {
31 + if (value.kind === "ObjectMethod") {
32 + objectMethodDecls.add(lvalue.identifier);
33 + } else if (value.kind === "ObjectExpression") {
34 + for (const operand of eachInstructionValueOperand(value)) {
35 + if (objectMethodDecls.has(operand.identifier)) {
36 + const operandScope = operand.identifier.scope;
37 + const lvalueScope = lvalue.identifier.scope;
38 +
39 + CompilerError.invariant(
40 + operandScope != null && lvalueScope != null,
41 + {
42 + reason:
43 + "Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.",
44 + suggestions: null,
45 + loc: GeneratedSource,
46 + }
47 + );
48 + mergeScopesBuilder.union([operandScope, lvalueScope]);
49 + }
50 + }
51 + }
52 + }
53 + }
54 + return mergeScopesBuilder;
55 +}
56 +
57 +export function alignObjectMethodScopes(fn: HIRFunction): void {
58 + // Handle inner functions: we assume that Scopes are disjoint across functions
59 + for (const [_, block] of fn.body.blocks) {
60 + for (const { value } of block.instructions) {
61 + if (
62 + value.kind === "ObjectMethod" ||
63 + value.kind === "FunctionExpression"
64 + ) {
65 + alignObjectMethodScopes(value.loweredFunc.func);
66 + }
67 + }
68 + }
69 +
70 + const scopeGroupsMap = findScopesToMerge(fn).canonicalize();
71 + /**
72 + * Step 1: Merge affected scopes to their canonical root.
73 + */
74 + for (const [scope, root] of scopeGroupsMap) {
75 + if (scope !== root) {
76 + root.range.start = makeInstructionId(
77 + Math.min(scope.range.start, root.range.start)
78 + );
79 + root.range.end = makeInstructionId(
80 + Math.max(scope.range.end, root.range.end)
81 + );
82 + }
83 + }
84 +
85 + /**
86 + * Step 2: Repoint identifiers whose scopes were merged.
87 + */
88 + for (const [_, block] of fn.body.blocks) {
89 + for (const {
90 + lvalue: { identifier },
91 + } of block.instructions) {
92 + if (identifier.scope != null) {
93 + const root = scopeGroupsMap.get(identifier.scope);
94 + if (root != null) {
95 + identifier.scope = root;
96 + }
97 + // otherwise, this identifier's scope was not affected by this pass
98 + }
99 + }
100 + }
101 +}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/FlattenScopesWithObjectMethods.ts deleted
-58
@@ -1,58 +0,0 @@
1 -/*
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import {
9 - InstructionId,
10 - ReactiveFunction,
11 - ReactiveScopeBlock,
12 - ReactiveStatement,
13 - ReactiveValue,
14 -} from "../HIR";
15 -import {
16 - ReactiveFunctionTransform,
17 - Transformed,
18 - visitReactiveFunction,
19 -} from "./visitors";
20 -
21 -export function flattenScopesWithObjectMethods(fn: ReactiveFunction): void {
22 - visitReactiveFunction(fn, new Transform(), {
23 - hasObjectMethod: false,
24 - });
25 -}
26 -
27 -type State = {
28 - hasObjectMethod: boolean;
29 -};
30 -
31 -class Transform extends ReactiveFunctionTransform<State> {
32 - override transformScope(
33 - scope: ReactiveScopeBlock,
34 - outerState: State
35 - ): Transformed<ReactiveStatement> {
36 - const innerState: State = {
37 - hasObjectMethod: false,
38 - };
39 - this.visitScope(scope, innerState);
40 - outerState.hasObjectMethod ||= innerState.hasObjectMethod;
41 - if (innerState.hasObjectMethod) {
42 - return { kind: "replace-many", value: scope.instructions };
43 - } else {
44 - return { kind: "keep" };
45 - }
46 - }
47 -
48 - override visitValue(
49 - id: InstructionId,
50 - value: ReactiveValue,
51 - state: State
52 - ): void {
53 - this.traverseValue(id, value, state);
54 - if (value.kind === "ObjectMethod") {
55 - state.hasObjectMethod = true;
56 - }
57 - }
58 -}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/index.ts
+1 -1
@@ -5,6 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +export { alignObjectMethodScopes } from "./AlignObjectMethodScopes";
9 export { alignReactiveScopesToBlockScopes } from "./AlignReactiveScopesToBlockScopes";
10 export { assertScopeInstructionsWithinScopes } from "./AssertScopeInstructionsWithinScope";
11 export { buildReactiveBlocks } from "./BuildReactiveBlocks";
@@ -16,7 +17,6 @@ export {
17 export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclarationsFromDestructuring";
18 export { flattenReactiveLoops } from "./FlattenReactiveLoops";
19 export { flattenScopesWithHooks } from "./FlattenScopesWithHooks";
19 -export { flattenScopesWithObjectMethods } from "./FlattenScopesWithObjectMethods";
20 export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
21 export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
22 export { mergeOverlappingReactiveScopes } from "./MergeOverlappingReactiveScopes";
compiler/packages/babel-plugin-react-forget/src/Utils/utils.ts
+6
@@ -68,6 +68,12 @@ export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
68 return union;
69 }
70
71 +export function nonNull<T extends NonNullable<U>, U>(
72 + value: T | null | undefined
73 +): value is T {
74 + return value != null;
75 +}
76 +
77 export function hasNode<T>(
78 input: NodePath<T | null | undefined>
79 ): input is NodePath<NonNullable<T>> {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-literal-method-derived-in-ternary-consequent.expect.md
+18 -7
@@ -24,17 +24,28 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Code
25
26 ```javascript
27 +import { unstable_useMemoCache as useMemoCache } from "react";
28 import { identity, createHookWrapper } from "shared-runtime";
29
30 function useHook(t17) {
31 + const $ = useMemoCache(3);
32 const { isCond, value } = t17;
31 - return isCond
32 - ? identity({
33 - getValue() {
34 - return value;
35 - },
36 - })
37 - : 42;
33 + let t0;
34 + if ($[0] !== isCond || $[1] !== value) {
35 + t0 = isCond
36 + ? identity({
37 + getValue() {
38 + return value;
39 + },
40 + })
41 + : 42;
42 + $[0] = isCond;
43 + $[1] = value;
44 + $[2] = t0;
45 + } else {
46 + t0 = $[2];
47 + }
48 + return t0;
49 }
50
51 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-literal-method-in-ternary-consequent.expect.md
+18 -7
@@ -24,17 +24,28 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Code
25
26 ```javascript
27 +import { unstable_useMemoCache as useMemoCache } from "react";
28 import { createHookWrapper } from "shared-runtime";
29
30 function useHook(t15) {
31 + const $ = useMemoCache(3);
32 const { isCond, value } = t15;
31 - return isCond
32 - ? {
33 - getValue() {
34 - return value;
35 - },
36 - }
37 - : 42;
33 + let t0;
34 + if ($[0] !== isCond || $[1] !== value) {
35 + t0 = isCond
36 + ? {
37 + getValue() {
38 + return value;
39 + },
40 + }
41 + : 42;
42 + $[0] = isCond;
43 + $[1] = value;
44 + $[2] = t0;
45 + } else {
46 + t0 = $[2];
47 + }
48 + return t0;
49 }
50
51 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-maybe-alias.expect.md
+22 -11
@@ -27,20 +27,31 @@ export const FIXTURE_ENTRYPOINT = {
27 ## Code
28
29 ```javascript
30 +import { unstable_useMemoCache as useMemoCache } from "react";
31 import { createHookWrapper, setProperty } from "shared-runtime";
32 function useHook(props) {
32 - const x = {
33 - getX() {
34 - return props;
35 - },
36 - };
33 + const $ = useMemoCache(2);
34 + let t0;
35 + if ($[0] !== props) {
36 + const x = {
37 + getX() {
38 + return props;
39 + },
40 + };
41
38 - const y = {
39 - getY() {
40 - return "y";
41 - },
42 - };
43 - return setProperty(x, y);
42 + const y = {
43 + getY() {
44 + return "y";
45 + },
46 + };
47 +
48 + t0 = setProperty(x, y);
49 + $[0] = props;
50 + $[1] = t0;
51 + } else {
52 + t0 = $[1];
53 + }
54 + return t0;
55 }
56
57 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md
+19 -8
@@ -25,17 +25,28 @@ export const FIXTURE_ENTRYPOINT = {
25 ## Code
26
27 ```javascript
28 +import { unstable_useMemoCache as useMemoCache } from "react";
29 import { createHookWrapper, mutate } from "shared-runtime";
30
31 function useHook(a) {
31 - const x = { a };
32 - const obj = {
33 - method() {
34 - mutate(x);
35 - return x;
36 - },
37 - };
38 - return obj.method();
32 + const $ = useMemoCache(2);
33 + let t0;
34 + if ($[0] !== a) {
35 + const x = { a };
36 + const obj = {
37 + method() {
38 + mutate(x);
39 + return x;
40 + },
41 + };
42 +
43 + t0 = obj.method();
44 + $[0] = a;
45 + $[1] = t0;
46 + } else {
47 + t0 = $[1];
48 + }
49 + return t0;
50 }
51
52 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-aliased-mutate-after.expect.md
+10 -10
@@ -27,23 +27,23 @@ export const FIXTURE_ENTRYPOINT = {
27 import { unstable_useMemoCache as useMemoCache } from "react";
28 import { createHookWrapper, mutate, mutateAndReturn } from "shared-runtime";
29 function useHook(t21) {
30 - const $ = useMemoCache(1);
30 + const $ = useMemoCache(2);
31 const { value } = t21;
32 - const x = mutateAndReturn({ value });
33 - let t0;
34 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35 - t0 = {
32 + let obj;
33 + if ($[0] !== value) {
34 + const x = mutateAndReturn({ value });
35 + obj = {
36 getValue() {
37 return value;
38 },
39 };
40 - $[0] = t0;
40 +
41 + mutate(x);
42 + $[0] = value;
43 + $[1] = obj;
44 } else {
42 - t0 = $[0];
45 + obj = $[1];
46 }
44 - const obj = t0;
45 -
46 - mutate(x);
47 return obj;
48 }
49
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-derived-value.expect.md
+5 -4
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
26 import { unstable_useMemoCache as useMemoCache } from "react";
27 import { createHookWrapper, mutateAndReturn } from "shared-runtime";
28 function useHook(t18) {
29 - const $ = useMemoCache(3);
29 + const $ = useMemoCache(4);
30 const { value } = t18;
31 let t0;
32 if ($[0] !== value) {
@@ -38,15 +38,16 @@ function useHook(t18) {
38 }
39 const x = t0;
40 let t1;
41 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
41 + if ($[2] !== x) {
42 t1 = {
43 getValue() {
44 return x;
45 },
46 };
47 - $[2] = t1;
47 + $[2] = x;
48 + $[3] = t1;
49 } else {
49 - t1 = $[2];
50 + t1 = $[3];
51 }
52 const obj = t1;
53 return obj;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-hook-dep.expect.md
+5 -4
@@ -26,18 +26,19 @@ export const FIXTURE_ENTRYPOINT = {
26 import { createHookWrapper } from "shared-runtime";
27 import { useState, unstable_useMemoCache as useMemoCache } from "react";
28 function useFoo() {
29 - const $ = useMemoCache(1);
29 + const $ = useMemoCache(2);
30 const [state] = useState(false);
31 let t0;
32 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32 + if ($[0] !== state) {
33 t0 = {
34 func() {
35 return state;
36 },
37 };
38 - $[0] = t0;
38 + $[0] = state;
39 + $[1] = t0;
40 } else {
40 - t0 = $[0];
41 + t0 = $[1];
42 }
43 return t0;
44 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-mutated-after.expect.md
+17 -8
@@ -24,17 +24,26 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Code
25
26 ```javascript
27 +import { unstable_useMemoCache as useMemoCache } from "react";
28 import { createHookWrapper, mutate, mutateAndReturn } from "shared-runtime";
29 function useHook(t21) {
30 + const $ = useMemoCache(2);
31 const { value } = t21;
30 - const x = mutateAndReturn({ value });
31 - const obj = {
32 - getValue() {
33 - return x;
34 - },
35 - };
36 -
37 - mutate(obj);
32 + let obj;
33 + if ($[0] !== value) {
34 + const x = mutateAndReturn({ value });
35 + obj = {
36 + getValue() {
37 + return x;
38 + },
39 + };
40 +
41 + mutate(obj);
42 + $[0] = value;
43 + $[1] = obj;
44 + } else {
45 + obj = $[1];
46 + }
47 return obj;
48 }
49
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-1.expect.md
+6 -5
@@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = {
27 import { unstable_useMemoCache as useMemoCache } from "react";
28 import { createHookWrapper } from "shared-runtime";
29 function useHook(t16) {
30 - const $ = useMemoCache(4);
30 + const $ = useMemoCache(5);
31 const { a, b } = t16;
32 let t0;
33 if ($[0] !== a) {
@@ -40,17 +40,18 @@ function useHook(t16) {
40 t0 = $[1];
41 }
42 let t1;
43 - if ($[2] !== t0) {
43 + if ($[2] !== b || $[3] !== t0) {
44 t1 = {
45 x: t0,
46 y() {
47 return [b];
48 },
49 };
50 - $[2] = t0;
51 - $[3] = t1;
50 + $[2] = b;
51 + $[3] = t0;
52 + $[4] = t1;
53 } else {
53 - t1 = $[3];
54 + t1 = $[4];
55 }
56 return t1;
57 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-2.expect.md
+14 -13
@@ -28,7 +28,7 @@ import { unstable_useMemoCache as useMemoCache } from "react";
28 import { createHookWrapper } from "shared-runtime";
29
30 function useHook(t16) {
31 - const $ = useMemoCache(7);
31 + const $ = useMemoCache(8);
32 const { a, b, c } = t16;
33 let t0;
34 if ($[0] !== a) {
@@ -38,16 +38,16 @@ function useHook(t16) {
38 } else {
39 t0 = $[1];
40 }
41 - let t1;
42 - if ($[2] !== c) {
43 - t1 = { c };
44 - $[2] = c;
45 - $[3] = t1;
46 - } else {
47 - t1 = $[3];
48 - }
41 let t2;
50 - if ($[4] !== t0 || $[5] !== t1) {
42 + if ($[2] !== b || $[3] !== c || $[4] !== t0) {
43 + let t1;
44 + if ($[6] !== c) {
45 + t1 = { c };
46 + $[6] = c;
47 + $[7] = t1;
48 + } else {
49 + t1 = $[7];
50 + }
51 t2 = {
52 x: t0,
53 y() {
@@ -55,11 +55,12 @@ function useHook(t16) {
55 },
56 z: t1,
57 };
58 + $[2] = b;
59 + $[3] = c;
60 $[4] = t0;
59 - $[5] = t1;
60 - $[6] = t2;
61 + $[5] = t2;
62 } else {
62 - t2 = $[6];
63 + t2 = $[5];
64 }
65 return t2;
66 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-nested.expect.md
+6 -4
@@ -35,11 +35,11 @@ import { useState, unstable_useMemoCache as useMemoCache } from "react";
35 import { createHookWrapper } from "shared-runtime";
36
37 function useHook(t21) {
38 - const $ = useMemoCache(1);
38 + const $ = useMemoCache(3);
39 const { value } = t21;
40 const [state] = useState(false);
41 let t0;
42 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
42 + if ($[0] !== value || $[1] !== state) {
43 t0 = {
44 getX() {
45 return {
@@ -51,9 +51,11 @@ function useHook(t21) {
51 };
52 },
53 };
54 - $[0] = t0;
54 + $[0] = value;
55 + $[1] = state;
56 + $[2] = t0;
57 } else {
56 - t0 = $[0];
58 + t0 = $[2];
59 }
60 return t0;
61 }