@samitouri / QOS-React-1 / commits / e49a62c15d

[hir] Do not memoize object methods separately

Object methods must not be cached independently, so this PR flattens the reactive scope to prevent memoization. In the future, we can combine the FlattenScopesWithObjectMethods and FlattedScopesWithHooks passes by making them more modular. But this works for now.

Sathya Gunasekaran committed Oct 2, 2023 at 20:22 UTC e49a62c15d761ae7652e58e405a30dd2f8bd82be
15 files changed +301 -48
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+8
@@ -40,6 +40,7 @@ import {
40 extractScopeDeclarationsFromDestructuring,
41 flattenReactiveLoops,
42 flattenScopesWithHooks,
43 + flattenScopesWithObjectMethods,
44 inferReactiveScopeVariables,
45 memoizeFbtOperandsInSameScope,
46 mergeConsecutiveScopes,
@@ -230,6 +231,13 @@ export function* run(
231 value: reactiveFunction,
232 });
233
234 + flattenScopesWithObjectMethods(reactiveFunction);
235 + yield log({
236 + kind: "reactive",
237 + name: "FlattenScopesWithObjectMethods",
238 + value: reactiveFunction,
239 + });
240 +
241 propagateScopeDependencies(reactiveFunction);
242 yield log({
243 kind: "reactive",
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+4
@@ -1083,6 +1083,10 @@ export function makeInstructionId(id: number): InstructionId {
1083 return id as InstructionId;
1084 }
1085
1086 +export function isObjectMethodType(id: Identifier): boolean {
1087 + return id.type.kind == "ObjectMethod";
1088 +}
1089 +
1090 export function isObjectType(id: Identifier): boolean {
1091 return id.type.kind === "Object";
1092 }
compiler/packages/babel-plugin-react-forget/src/HIR/Types.ts
+18 -2
@@ -9,7 +9,13 @@ import { CompilerError } from "../CompilerError";
9
10 export type BuiltInType = PrimitiveType | FunctionType | ObjectType;
11
12 -export type Type = BuiltInType | PhiType | TypeVar | PolyType | PropType;
12 +export type Type =
13 + | BuiltInType
14 + | PhiType
15 + | TypeVar
16 + | PolyType
17 + | PropType
18 + | ObjectMethod;
19 export type PrimitiveType = { kind: "Primitive" };
20
21 /**
@@ -54,6 +60,11 @@ export type PropType = {
60 object: Type;
61 propertyName: string;
62 };
63 +
64 +export type ObjectMethod = {
65 + kind: "ObjectMethod";
66 +};
67 +
68 /**
69 * Simulated opaque type for TypeId to prevent using normal numbers as ids
70 * accidentally.
@@ -87,7 +98,8 @@ export function typeEquals(tA: Type, tB: Type): boolean {
98 objectTypeEquals(tA, tB) ||
99 primitiveTypeEquals(tA, tB) ||
100 polyTypeEquals(tA, tB) ||
90 - phiTypeEquals(tA, tB)
101 + phiTypeEquals(tA, tB) ||
102 + objectMethodTypeEquals(tA, tB)
103 );
104 }
105
@@ -102,6 +114,10 @@ function typeKindCheck(tA: Type, tb: Type, type: string): boolean {
114 return tA.kind === type && tb.kind === type;
115 }
116
117 +function objectMethodTypeEquals(tA: Type, tB: Type): boolean {
118 + return typeKindCheck(tA, tB, "ObjectMethod");
119 +}
120 +
121 function primitiveTypeEquals(tA: Type, tB: Type): boolean {
122 return typeKindCheck(tA, tB, "Primitive");
123 }
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/FlattenScopesWithObjectMethods.ts new
+58
@@ -0,0 +1,58 @@
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/PropagateScopeDependencies.ts
+7
@@ -11,6 +11,7 @@ import {
11 IdentifierId,
12 InstructionId,
13 InstructionKind,
14 + isObjectMethodType,
15 isRefValueType,
16 isUseRefType,
17 makeInstructionId,
@@ -330,6 +331,12 @@ class Context {
331 return false;
332 }
333
334 + // object methods are not deps because they will be codegen'ed back in to
335 + // the object literal.
336 + if (isObjectMethodType(maybeDependency.identifier)) {
337 + return false;
338 + }
339 +
340 const identifier = maybeDependency.identifier;
341 // If this operand is used in a scope, has a dynamic value, and was defined
342 // before this scope, then its a dependency of the scope.
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/index.ts
+1
@@ -15,6 +15,7 @@ export {
15 export { extractScopeDeclarationsFromDestructuring } from "./ExtractScopeDeclarationsFromDestructuring";
16 export { flattenReactiveLoops } from "./FlattenReactiveLoops";
17 export { flattenScopesWithHooks } from "./FlattenScopesWithHooks";
18 +export { flattenScopesWithObjectMethods } from "./FlattenScopesWithObjectMethods";
19 export { inferReactiveScopeVariables } from "./InferReactiveScopeVariables";
20 export { memoizeFbtOperandsInSameScope } from "./MemoizeFbtOperandsInSameScope";
21 export { mergeConsecutiveScopes } from "./MergeConsecutiveScopes";
compiler/packages/babel-plugin-react-forget/src/TypeInference/InferTypes.ts
+5 -1
@@ -255,7 +255,6 @@ function* generateInstructionTypes(
255 break;
256 }
257
258 - case "ObjectMethod":
258 case "FunctionExpression": {
259 yield* generate(value.loweredFunc.func);
260 break;
@@ -266,6 +265,11 @@ function* generateInstructionTypes(
265 break;
266 }
267
268 + case "ObjectMethod": {
269 + yield equation(left, { kind: "ObjectMethod" });
270 + break;
271 + }
272 +
273 case "DeclareLocal":
274 case "NewExpression":
275 case "JsxExpression":
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { mutate } from "shared-runtime";
6 +
7 +function Component(a) {
8 + const x = { a };
9 + let obj = {
10 + method() {
11 + mutate(x);
12 + return x.a;
13 + },
14 + };
15 + return obj.method();
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ x: 1 }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { mutate } from "shared-runtime";
29 +
30 +function Component(a) {
31 + const x = { a };
32 + const obj = {
33 + method() {
34 + mutate(x);
35 + return x.a;
36 + },
37 + };
38 + return obj.method();
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{ x: 1 }],
44 +};
45 +
46 +```
47 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-3.expect.md~051f3e57 ([hir] Do not memoize object methods separately) new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { mutate } from "shared-runtime";
6 +
7 +function Component(a) {
8 + const x = { a };
9 + let obj = {
10 + method() {
11 + mutate(x);
12 + return x;
13 + },
14 + };
15 + return obj.method();
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{ x: 1 }, { a: 2 }, { b: 2 }],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { mutate } from "shared-runtime";
29 +
30 +function Component(a) {
31 + const x = { a };
32 + const obj = {
33 + method() {
34 + mutate(x);
35 + return x;
36 + },
37 + };
38 + return obj.method();
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{ x: 1 }, { a: 2 }, { b: 2 }],
44 +};
45 +
46 +```
47 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand-3.js new
+17
@@ -0,0 +1,17 @@
1 +import { mutate } from "shared-runtime";
2 +
3 +function Component(a) {
4 + const x = { a };
5 + let obj = {
6 + method() {
7 + mutate(x);
8 + return x.a;
9 + },
10 + };
11 + return obj.method();
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ x: 1 }],
17 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand.expect.md new
+49
@@ -0,0 +1,49 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + let obj = {
7 + method() {
8 + return 1;
9 + },
10 + };
11 + return obj.method();
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{ x: 1 }, { a: 2 }, { b: 2 }],
17 +};
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +import { unstable_useMemoCache as useMemoCache } from "react";
25 +function Component() {
26 + const $ = useMemoCache(1);
27 + let t0;
28 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
29 + const obj = {
30 + method() {
31 + return 1;
32 + },
33 + };
34 +
35 + t0 = obj.method();
36 + $[0] = t0;
37 + } else {
38 + t0 = $[0];
39 + }
40 + return t0;
41 +}
42 +
43 +export const FIXTURE_ENTRYPOINT = {
44 + fn: Component,
45 + params: [{ x: 1 }, { a: 2 }, { b: 2 }],
46 +};
47 +
48 +```
49 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-method-shorthand.js new
+13
@@ -0,0 +1,13 @@
1 +function Component() {
2 + let obj = {
3 + method() {
4 + return 1;
5 + },
6 + };
7 + return obj.method();
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Component,
12 + params: [{ x: 1 }, { a: 2 }, { b: 2 }],
13 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-1.expect.md
+11 -20
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @debug
6 function Component({ a, b }) {
7 return {
8 x: function () {
@@ -15,7 +16,7 @@ function Component({ a, b }) {
16
17 export const FIXTURE_ENTRYPOINT = {
18 fn: Component,
18 - params: [{ x: 1 }, { a: 2 }, { b: 2 }],
19 + params: [{ x: 1 }, { a: 2 }],
20 };
21
22 ```
@@ -23,9 +24,9 @@ export const FIXTURE_ENTRYPOINT = {
24 ## Code
25
26 ```javascript
26 -import { unstable_useMemoCache as useMemoCache } from "react";
27 +import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
28 function Component(t16) {
28 - const $ = useMemoCache(7);
29 + const $ = useMemoCache(4);
30 const { a, b } = t16;
31 const c_0 = $[0] !== a;
32 let t0;
@@ -38,36 +39,26 @@ function Component(t16) {
39 } else {
40 t0 = $[1];
41 }
41 - const c_2 = $[2] !== b;
42 + const c_2 = $[2] !== t0;
43 let t1;
44 if (c_2) {
44 - $[2] = b;
45 - $[3] = t1;
46 - } else {
47 - t1 = $[3];
48 - }
49 - const c_4 = $[4] !== t0;
50 - const c_5 = $[5] !== t1;
51 - let t2;
52 - if (c_4 || c_5) {
53 - t2 = {
45 + t1 = {
46 x: t0,
47 y() {
48 return [b];
49 },
50 };
59 - $[4] = t0;
60 - $[5] = t1;
61 - $[6] = t2;
51 + $[2] = t0;
52 + $[3] = t1;
53 } else {
63 - t2 = $[6];
54 + t1 = $[3];
55 }
65 - return t2;
56 + return t1;
57 }
58
59 export const FIXTURE_ENTRYPOINT = {
60 fn: Component,
70 - params: [{ x: 1 }, { a: 2 }, { b: 2 }],
61 + params: [{ x: 1 }, { a: 2 }],
62 };
63
64 ```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-1.js
+2 -1
@@ -1,3 +1,4 @@
1 +// @debug
2 function Component({ a, b }) {
3 return {
4 x: function () {
@@ -11,5 +12,5 @@ function Component({ a, b }) {
12
13 export const FIXTURE_ENTRYPOINT = {
14 fn: Component,
14 - params: [{ x: 1 }, { a: 2 }, { b: 2 }],
15 + params: [{ x: 1 }, { a: 2 }],
16 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-shorthand-method-2.expect.md
+14 -24
@@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = {
24 ```javascript
25 import { unstable_useMemoCache as useMemoCache } from "react";
26 function Component(t16) {
27 - const $ = useMemoCache(10);
27 + const $ = useMemoCache(7);
28 const { a, b, c } = t16;
29 const c_0 = $[0] !== a;
30 let t0;
@@ -35,43 +35,33 @@ function Component(t16) {
35 } else {
36 t0 = $[1];
37 }
38 - const c_2 = $[2] !== b;
38 + const c_2 = $[2] !== c;
39 let t1;
40 if (c_2) {
41 - $[2] = b;
41 + t1 = { c };
42 + $[2] = c;
43 $[3] = t1;
44 } else {
45 t1 = $[3];
46 }
46 - const c_4 = $[4] !== c;
47 + const c_4 = $[4] !== t0;
48 + const c_5 = $[5] !== t1;
49 let t2;
48 - if (c_4) {
49 - t2 = { c };
50 - $[4] = c;
51 - $[5] = t2;
52 - } else {
53 - t2 = $[5];
54 - }
55 - const c_6 = $[6] !== t0;
56 - const c_7 = $[7] !== t1;
57 - const c_8 = $[8] !== t2;
58 - let t3;
59 - if (c_6 || c_7 || c_8) {
60 - t3 = {
50 + if (c_4 || c_5) {
51 + t2 = {
52 x: t0,
53 y() {
54 return [b];
55 },
65 - z: t2,
56 + z: t1,
57 };
67 - $[6] = t0;
68 - $[7] = t1;
69 - $[8] = t2;
70 - $[9] = t3;
58 + $[4] = t0;
59 + $[5] = t1;
60 + $[6] = t2;
61 } else {
72 - t3 = $[9];
62 + t2 = $[6];
63 }
74 - return t3;
64 + return t2;
65 }
66
67 export const FIXTURE_ENTRYPOINT = {