@samitouri / QOS-React / commits / 2fa04d128e

Transitive mixed type, take 2

This is a redo of #1640 now that we've established the necessary infrastructure, most notably `Effect.ConditionallyMutate` and `noAlias` from #2103 earlier in this stack. We can now understand the semantics of hooks that return deeply readonly values composed of primitives, arrays, or objects such that any `.map()` or `.filter()` calls are guaranteed to be the corresponding array methods. That further allows us to refine, since we know that the lambdas passed to these calls can't alias, are conditionally mutable, etc. All in all this should let us memoize less in practice.

Joe Savona committed Sep 18, 2023 at 16:42 UTC 2fa04d128e47abb27645a90873b08369e82c9ee7
13 files changed +373 -94
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+26 -2
@@ -28,6 +28,7 @@ import {
28 makeIdentifierId,
29 } from "./HIR";
30 import {
31 + BuiltInMixedReadonlyId,
32 DefaultMutatingHook,
33 DefaultNonmutatingHook,
34 FunctionSignature,
@@ -55,6 +56,25 @@ export type Hook = {
56 * compiler to avoid memoizing arguments.
57 */
58 noAlias?: boolean;
59 +
60 + /**
61 + * Specifies whether the hook returns data that is composed of:
62 + * - undefined
63 + * - null
64 + * - boolean
65 + * - number
66 + * - string
67 + * - arrays whose items are also transitiveMixed
68 + * - objects whose values are also transitiveMixed
69 + *
70 + * Many state management and data-fetching APIs return data that meets
71 + * this criteria since this is JSON + undefined. Forget can compile
72 + * hooks that return transitively mixed data more optimally because it
73 + * can make inferences about some method calls (especially array methods
74 + * like `data.items.map(...)` since these builtin types have few built-in
75 + * methods.
76 + */
77 + transitiveMixedData?: boolean;
78 };
79
80 // TODO(mofeiZ): User defined global types (with corresponding shapes).
@@ -236,7 +256,9 @@ export class Environment {
256 addHook(this.#shapes, [], {
257 positionalParams: [],
258 restParam: hook.effectKind,
239 - returnType: { kind: "Poly" },
259 + returnType: hook.transitiveMixedData
260 + ? { kind: "Object", shapeId: BuiltInMixedReadonlyId }
261 + : { kind: "Poly" },
262 returnValueKind: hook.valueKind,
263 calleeEffect: Effect.Read,
264 hookKind: "Custom",
@@ -312,7 +334,9 @@ export class Environment {
334 loc: null,
335 suggestions: null,
336 });
315 - return shape.properties.get(property) ?? null;
337 + return (
338 + shape.properties.get(property) ?? shape.properties.get("*") ?? null
339 + );
340 } else {
341 return null;
342 }
compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
+37
@@ -170,6 +170,7 @@ export const BuiltInUseStateId = "BuiltInUseState";
170 export const BuiltInSetStateId = "BuiltInSetState";
171 export const BuiltInUseRefId = "BuiltInUseRefId";
172 export const BuiltInRefValueId = "BuiltInRefValue";
173 +export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly";
174
175 /**
176 * ShapeRegistry with default definitions for built-ins.
@@ -294,6 +295,42 @@ addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
295
296 addObject(BUILTIN_SHAPES, BuiltInRefValueId, []);
297
298 +addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
299 + [
300 + "toString",
301 + addFunction(BUILTIN_SHAPES, [], {
302 + positionalParams: [],
303 + restParam: Effect.Read,
304 + returnType: PRIMITIVE_TYPE,
305 + calleeEffect: Effect.Read,
306 + returnValueKind: ValueKind.Immutable,
307 + }),
308 + ],
309 + [
310 + "map",
311 + addFunction(BUILTIN_SHAPES, [], {
312 + positionalParams: [],
313 + restParam: Effect.Read,
314 + returnType: { kind: "Object", shapeId: BuiltInArrayId },
315 + calleeEffect: Effect.ConditionallyMutate,
316 + returnValueKind: ValueKind.Mutable,
317 + noAlias: true,
318 + }),
319 + ],
320 + [
321 + "filter",
322 + addFunction(BUILTIN_SHAPES, [], {
323 + positionalParams: [],
324 + restParam: Effect.Read,
325 + returnType: { kind: "Object", shapeId: BuiltInArrayId },
326 + calleeEffect: Effect.ConditionallyMutate,
327 + returnValueKind: ValueKind.Mutable,
328 + noAlias: true,
329 + }),
330 + ],
331 + ["*", { kind: "Object", shapeId: BuiltInMixedReadonlyId }],
332 +]);
333 +
334 export const DefaultMutatingHook = addHook(BUILTIN_SHAPES, [], {
335 positionalParams: [],
336 restParam: Effect.ConditionallyMutate,
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md
+60 -53
@@ -56,87 +56,94 @@ function useFragment(_arg1, _arg2) {
56 }
57
58 function Component(props) {
59 - const $ = useMemoCache(16);
60 - const post = useFragment(graphql`...`, props.post);
61 - const c_0 = $[0] !== post;
59 + const $ = useMemoCache(17);
60 + let t0;
61 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
62 + t0 = graphql`...`;
63 + $[0] = t0;
64 + } else {
65 + t0 = $[0];
66 + }
67 + const post = useFragment(t0, props.post);
68 + const c_1 = $[1] !== post;
69 let media;
70 let allUrls;
71 let onClick;
65 - if (c_0) {
72 + if (c_1) {
73 allUrls = [];
74
68 - const { media: t0, comments: t2, urls: t4 } = post;
69 - const c_4 = $[4] !== t0;
70 - let t1;
71 - if (c_4) {
72 - t1 = t0 === undefined ? null : t0;
73 - $[4] = t0;
75 + const { media: t1, comments: t3, urls: t5 } = post;
76 + const c_5 = $[5] !== t1;
77 + let t2;
78 + if (c_5) {
79 + t2 = t1 === undefined ? null : t1;
80 $[5] = t1;
81 + $[6] = t2;
82 } else {
76 - t1 = $[5];
83 + t2 = $[6];
84 }
78 - media = t1;
79 - const c_6 = $[6] !== t2;
80 - let t3;
81 - if (c_6) {
82 - t3 = t2 === undefined ? [] : t2;
83 - $[6] = t2;
85 + media = t2;
86 + const c_7 = $[7] !== t3;
87 + let t4;
88 + if (c_7) {
89 + t4 = t3 === undefined ? [] : t3;
90 $[7] = t3;
91 + $[8] = t4;
92 } else {
86 - t3 = $[7];
93 + t4 = $[8];
94 }
88 - const comments = t3;
89 - const c_8 = $[8] !== t4;
90 - let t5;
91 - if (c_8) {
92 - t5 = t4 === undefined ? [] : t4;
93 - $[8] = t4;
95 + const comments = t4;
96 + const c_9 = $[9] !== t5;
97 + let t6;
98 + if (c_9) {
99 + t6 = t5 === undefined ? [] : t5;
100 $[9] = t5;
101 + $[10] = t6;
102 } else {
96 - t5 = $[9];
103 + t6 = $[10];
104 }
98 - const urls = t5;
99 - const c_10 = $[10] !== comments.length;
100 - let t6;
101 - if (c_10) {
102 - t6 = (e) => {
105 + const urls = t6;
106 + const c_11 = $[11] !== comments.length;
107 + let t7;
108 + if (c_11) {
109 + t7 = (e) => {
110 if (!comments.length) {
111 return;
112 }
113
114 console.log(comments.length);
115 };
109 - $[10] = comments.length;
110 - $[11] = t6;
116 + $[11] = comments.length;
117 + $[12] = t7;
118 } else {
112 - t6 = $[11];
119 + t7 = $[12];
120 }
114 - onClick = t6;
121 + onClick = t7;
122
123 allUrls.push(...urls);
117 - $[0] = post;
118 - $[1] = media;
119 - $[2] = allUrls;
120 - $[3] = onClick;
124 + $[1] = post;
125 + $[2] = media;
126 + $[3] = allUrls;
127 + $[4] = onClick;
128 } else {
122 - media = $[1];
123 - allUrls = $[2];
124 - onClick = $[3];
129 + media = $[2];
130 + allUrls = $[3];
131 + onClick = $[4];
132 }
126 - const c_12 = $[12] !== media;
127 - const c_13 = $[13] !== allUrls;
128 - const c_14 = $[14] !== onClick;
129 - let t7;
130 - if (c_12 || c_13 || c_14) {
131 - t7 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
132 - $[12] = media;
133 - $[13] = allUrls;
134 - $[14] = onClick;
135 - $[15] = t7;
133 + const c_13 = $[13] !== media;
134 + const c_14 = $[14] !== allUrls;
135 + const c_15 = $[15] !== onClick;
136 + let t8;
137 + if (c_13 || c_14 || c_15) {
138 + t8 = <Stringify media={media} allUrls={allUrls} onClick={onClick} />;
139 + $[13] = media;
140 + $[14] = allUrls;
141 + $[15] = onClick;
142 + $[16] = t8;
143 } else {
137 - t7 = $[15];
144 + t8 = $[16];
145 }
139 - return t7;
146 + return t8;
147 }
148
149 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md
+34 -27
@@ -30,53 +30,60 @@ function Component(props) {
30 ```javascript
31 import { unstable_useMemoCache as useMemoCache } from "react";
32 function Component(props) {
33 - const $ = useMemoCache(8);
34 - const post = useFragment(graphql`...`, props.post);
35 - const c_0 = $[0] !== post;
33 + const $ = useMemoCache(9);
34 + let t0;
35 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36 + t0 = graphql`...`;
37 + $[0] = t0;
38 + } else {
39 + t0 = $[0];
40 + }
41 + const post = useFragment(t0, props.post);
42 + const c_1 = $[1] !== post;
43 let media;
44 let onClick;
38 - if (c_0) {
45 + if (c_1) {
46 const allUrls = [];
47
48 const { media: t83, comments, urls } = post;
49 media = t83;
43 - const c_3 = $[3] !== comments.length;
44 - let t0;
45 - if (c_3) {
46 - t0 = (e) => {
50 + const c_4 = $[4] !== comments.length;
51 + let t1;
52 + if (c_4) {
53 + t1 = (e) => {
54 if (!comments.length) {
55 return;
56 }
57
58 console.log(comments.length);
59 };
53 - $[3] = comments.length;
54 - $[4] = t0;
60 + $[4] = comments.length;
61 + $[5] = t1;
62 } else {
56 - t0 = $[4];
63 + t1 = $[5];
64 }
58 - onClick = t0;
65 + onClick = t1;
66
67 allUrls.push(...urls);
61 - $[0] = post;
62 - $[1] = media;
63 - $[2] = onClick;
68 + $[1] = post;
69 + $[2] = media;
70 + $[3] = onClick;
71 } else {
65 - media = $[1];
66 - onClick = $[2];
72 + media = $[2];
73 + onClick = $[3];
74 }
68 - const c_5 = $[5] !== media;
69 - const c_6 = $[6] !== onClick;
70 - let t1;
71 - if (c_5 || c_6) {
72 - t1 = <Media media={media} onClick={onClick} />;
73 - $[5] = media;
74 - $[6] = onClick;
75 - $[7] = t1;
75 + const c_6 = $[6] !== media;
76 + const c_7 = $[7] !== onClick;
77 + let t2;
78 + if (c_6 || c_7) {
79 + t2 = <Media media={media} onClick={onClick} />;
80 + $[6] = media;
81 + $[7] = onClick;
82 + $[8] = t2;
83 } else {
77 - t1 = $[7];
84 + t2 = $[8];
85 }
79 - return t1;
86 + return t2;
87 }
88
89 ```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/optional-call-logical.expect.md
+20 -2
@@ -12,9 +12,27 @@ function Component(props) {
12 ## Code
13
14 ```javascript
15 +import { unstable_useMemoCache as useMemoCache } from "react";
16 function Component(props) {
16 - const item = useFragment(graphql`...`, props.item);
17 - return item.items?.map((item_0) => renderItem(item_0)) ?? [];
17 + const $ = useMemoCache(3);
18 + let t0;
19 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
20 + t0 = graphql`...`;
21 + $[0] = t0;
22 + } else {
23 + t0 = $[0];
24 + }
25 + const item = useFragment(t0, props.item);
26 + const c_1 = $[1] !== item.items;
27 + let t1;
28 + if (c_1) {
29 + t1 = item.items?.map((item_0) => renderItem(item_0)) ?? [];
30 + $[1] = item.items;
31 + $[2] = t1;
32 + } else {
33 + t1 = $[2];
34 + }
35 + return t1;
36 }
37
38 ```
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNoAliasOptimizations
6 +function Component(props) {
7 + const x = makeObject();
8 + const user = useFragment(
9 + graphql`fragment Component_user on User { ... }`,
10 + props.user
11 + );
12 + const posts = user.timeline.posts.edges.nodes.map((node) => {
13 + x.y = true;
14 + return <Post post={node} />;
15 + });
16 + posts.push({});
17 + const count = posts.length;
18 + foo(count);
19 + return <>{posts}</>;
20 +}
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableNoAliasOptimizations
28 +function Component(props) {
29 + const $ = useMemoCache(3);
30 + const x = makeObject();
31 + const user = useFragment(
32 + graphql`fragment Component_user on User { ... }`,
33 + props.user
34 + );
35 + const posts = user.timeline.posts.edges.nodes.map((node) => {
36 + x.y = true;
37 + return <Post post={node} />;
38 + });
39 + let t0;
40 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
41 + t0 = {};
42 + $[0] = t0;
43 + } else {
44 + t0 = $[0];
45 + }
46 + posts.push(t0);
47 + const count = posts.length;
48 + foo(count);
49 + const c_1 = $[1] !== posts;
50 + let t1;
51 + if (c_1) {
52 + t1 = <>{posts}</>;
53 + $[1] = posts;
54 + $[2] = t1;
55 + } else {
56 + t1 = $[2];
57 + }
58 + return t1;
59 +}
60 +
61 +```
62 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js new
+16
@@ -0,0 +1,16 @@
1 +// @enableNoAliasOptimizations
2 +function Component(props) {
3 + const x = makeObject();
4 + const user = useFragment(
5 + graphql`fragment Component_user on User { ... }`,
6 + props.user
7 + );
8 + const posts = user.timeline.posts.edges.nodes.map((node) => {
9 + x.y = true;
10 + return <Post post={node} />;
11 + });
12 + posts.push({});
13 + const count = posts.length;
14 + foo(count);
15 + return <>{posts}</>;
16 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNoAliasOptimizations
6 +function Component(props) {
7 + const user = useFragment(
8 + graphql`fragment Component_user on User { ... }`,
9 + props.user
10 + );
11 + const posts = user.timeline.posts.edges.nodes.map((node) => (
12 + <Post post={node} />
13 + ));
14 + posts.push({});
15 + const count = posts.length;
16 + foo(count);
17 + return <>{posts}</>;
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { unstable_useMemoCache as useMemoCache } from "react"; // @enableNoAliasOptimizations
26 +function Component(props) {
27 + const $ = useMemoCache(5);
28 + const user = useFragment(
29 + graphql`fragment Component_user on User { ... }`,
30 + props.user
31 + );
32 + const c_0 = $[0] !== user.timeline.posts.edges.nodes;
33 + let posts;
34 + if (c_0) {
35 + posts = user.timeline.posts.edges.nodes.map((node) => <Post post={node} />);
36 + let t0;
37 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
38 + t0 = {};
39 + $[2] = t0;
40 + } else {
41 + t0 = $[2];
42 + }
43 + posts.push(t0);
44 + $[0] = user.timeline.posts.edges.nodes;
45 + $[1] = posts;
46 + } else {
47 + posts = $[1];
48 + }
49 + const count = posts.length;
50 + foo(count);
51 + const c_3 = $[3] !== posts;
52 + let t1;
53 + if (c_3) {
54 + t1 = <>{posts}</>;
55 + $[3] = posts;
56 + $[4] = t1;
57 + } else {
58 + t1 = $[4];
59 + }
60 + return t1;
61 +}
62 +
63 +```
64 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.js new
+14
@@ -0,0 +1,14 @@
1 +// @enableNoAliasOptimizations
2 +function Component(props) {
3 + const user = useFragment(
4 + graphql`fragment Component_user on User { ... }`,
5 + props.user
6 + );
7 + const posts = user.timeline.posts.edges.nodes.map((node) => (
8 + <Post post={node} />
9 + ));
10 + posts.push({});
11 + const count = posts.length;
12 + foo(count);
13 + return <>{posts}</>;
14 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md
+16 -9
@@ -20,20 +20,27 @@ function HomeDiscoStoreItemTileRating(props) {
20 ```javascript
21 import { unstable_useMemoCache as useMemoCache } from "react";
22 function HomeDiscoStoreItemTileRating(props) {
23 - const $ = useMemoCache(1);
23 + const $ = useMemoCache(3);
24 const item = useFragment();
25 + const c_0 = $[0] !== item;
26 let count;
26 - count = 0;
27 - const aggregates = item?.aggregates || [];
28 - aggregates.forEach((aggregate) => {
29 - count = count + (aggregate.count || 0);
30 - });
27 + if (c_0) {
28 + count = 0;
29 + const aggregates = item?.aggregates || [];
30 + aggregates.forEach((aggregate) => {
31 + count = count + (aggregate.count || 0);
32 + });
33 + $[0] = item;
34 + $[1] = count;
35 + } else {
36 + count = $[1];
37 + }
38 let t0;
32 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
39 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
40 t0 = <Text>{count}</Text>;
34 - $[0] = t0;
41 + $[2] = t0;
42 } else {
36 - t0 = $[0];
43 + t0 = $[2];
44 }
45 return t0;
46 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md
+10 -1
@@ -12,8 +12,17 @@ function Component(props) {
12 ## Code
13
14 ```javascript
15 +import { unstable_useMemoCache as useMemoCache } from "react";
16 function Component(props) {
16 - const user = useFragment(graphql`fragment on User { name }`, props.user);
17 + const $ = useMemoCache(1);
18 + let t0;
19 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
20 + t0 = graphql`fragment on User { name }`;
21 + $[0] = t0;
22 + } else {
23 + t0 = $[0];
24 + }
25 + const user = useFragment(t0, props.user);
26 return user.name;
27 }
28
compiler/packages/fixture-test-utils/src/compiler-utils.ts
+12
@@ -100,6 +100,17 @@ export function transformFixtureInput(
100 {
101 valueKind: "frozen" as ValueKind,
102 effectKind: "freeze" as Effect,
103 + transitiveMixedData: false,
104 + noAlias: false,
105 + },
106 + ],
107 + [
108 + "useFragment",
109 + {
110 + valueKind: "frozen" as ValueKind,
111 + effectKind: "freeze" as Effect,
112 + transitiveMixedData: true,
113 + noAlias: true,
114 },
115 ],
116 [
@@ -107,6 +118,7 @@ export function transformFixtureInput(
118 {
119 valueKind: "mutable" as ValueKind,
120 effectKind: "read" as Effect,
121 + transitiveMixedData: false,
122 noAlias: true,
123 },
124 ],
compiler/packages/sprout/src/SproutTodoFilter.ts
+2
@@ -413,6 +413,8 @@ const skipFilter = new Set([
413 "fbt-template-string-same-scope",
414 "component-declaration-basic.flow",
415 "nested-function-with-param-as-captured-dep",
416 + "readonly-object-method-calls",
417 + "readonly-object-method-calls-mutable-lambda",
418
419 // TODO: 🌲
420 "forest-basic",