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

InferReactivePlaces understands setState type

I realized this while working on Forest. When computing the dependencies of a reactive scope we can omit setState functions in the general case (exception described below). Currently that's implemented in PruneNonReactiveDependencies. However, this causes us to miss some optimizations — a value isn't reactive if its only dependency is a setState, and that may allow further downstreams values to become non-reactive. We lose out on that by only filtering out setStates in PruneNonReactiveDependencies — this logic really belongs in InferReactivePlaces. So this PR moves the check for setState types to that pass. The updated fixtures show that this already uncovers some wins. The _new_ fixtures covers the exception. It's possible for a value to be typed as being a setState function, but to still be reactive: if its a local that is conditionally assigned different setState function values. Currently this test happens to work because our phi type inference is incomplete (see #2296). I'm adding the test now though to prevent regressions when we fix phi type inference.

Joe Savona committed Dec 11, 2023 at 11:34 UTC fa8f47eb4167cdafb3bba325f2f0a05ff280eca1
8 files changed +172 -29
compiler/packages/babel-plugin-react-forget/src/Inference/InferReactivePlaces.ts
+4
@@ -15,6 +15,7 @@ import {
15 Place,
16 computePostDominatorTree,
17 getHookKind,
18 + isSetStateType,
19 } from "../HIR";
20 import { PostDominator } from "../HIR/Dominator";
21 import {
@@ -191,6 +192,9 @@ export function inferReactivePlaces(fn: HIRFunction): void {
192
193 if (hasReactiveInput) {
194 for (const lvalue of eachInstructionLValue(instruction)) {
195 + if (isSetStateType(lvalue.identifier)) {
196 + continue;
197 + }
198 reactiveIdentifiers.markReactive(lvalue);
199 }
200
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneNonReactiveDependencies.ts
+2 -8
@@ -5,12 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {
9 - IdentifierId,
10 - ReactiveFunction,
11 - ReactiveScopeBlock,
12 - isSetStateType,
13 -} from "../HIR";
8 +import { IdentifierId, ReactiveFunction, ReactiveScopeBlock } from "../HIR";
9 import { collectReactiveIdentifiers } from "./CollectReactiveIdentifiers";
10 import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors";
11
@@ -34,8 +29,7 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
29 ): void {
30 this.traverseScope(scope, state);
31 for (const dep of scope.scope.dependencies) {
37 - const isReactive =
38 - state.has(dep.identifier.id) && !isSetStateType(dep.identifier);
32 + const isReactive = state.has(dep.identifier.id);
33 if (!isReactive) {
34 scope.scope.dependencies.delete(dep);
35 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/concise-arrow-expr.expect.md
+4 -5
@@ -15,7 +15,7 @@ function component() {
15 ```javascript
16 import { unstable_useMemoCache as useMemoCache } from "react";
17 function component() {
18 - const $ = useMemoCache(3);
18 + const $ = useMemoCache(2);
19 const [x, setX] = useState(0);
20 let t0;
21 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -26,12 +26,11 @@ function component() {
26 }
27 const handler = t0;
28 let t1;
29 - if ($[1] !== handler) {
29 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
30 t1 = <Foo handler={handler} />;
31 - $[1] = handler;
32 - $[2] = t1;
31 + $[1] = t1;
32 } else {
34 - t1 = $[2];
33 + t1 = $[1];
34 }
35 return t1;
36 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/controlled-input.expect.md
+5 -6
@@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = {
22 ```javascript
23 import { useState, unstable_useMemoCache as useMemoCache } from "react";
24 function component() {
25 - const $ = useMemoCache(4);
25 + const $ = useMemoCache(3);
26 const [x, setX] = useState(0);
27 let t0;
28 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -33,13 +33,12 @@ function component() {
33 }
34 const handler = t0;
35 let t1;
36 - if ($[1] !== handler || $[2] !== x) {
36 + if ($[1] !== x) {
37 t1 = <input onChange={handler} value={x} />;
38 - $[1] = handler;
39 - $[2] = x;
40 - $[3] = t1;
38 + $[1] = x;
39 + $[2] = t1;
40 } else {
42 - t1 = $[3];
41 + t1 = $[2];
42 }
43 return t1;
44 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/inadvertent-mutability-readonly-lambda.expect.md
+4 -5
@@ -23,7 +23,7 @@ function Component(props) {
23 ```javascript
24 import { unstable_useMemoCache as useMemoCache } from "react";
25 function Component(props) {
26 - const $ = useMemoCache(3);
26 + const $ = useMemoCache(2);
27 const [value, setValue] = useState(null);
28 let t0;
29 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -36,13 +36,12 @@ function Component(props) {
36
37 useOtherHook();
38 let x;
39 - if ($[1] !== onChange) {
39 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
40 x = {};
41 foo(x, onChange);
42 - $[1] = onChange;
43 - $[2] = x;
42 + $[1] = x;
43 } else {
45 - x = $[2];
44 + x = $[1];
45 }
46 return x;
47 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md
+4 -5
@@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = {
26 ```javascript
27 import { unstable_useMemoCache as useMemoCache } from "react";
28 function Component(props) {
29 - const $ = useMemoCache(4);
29 + const $ = useMemoCache(3);
30 const [x, setX] = useState(null);
31 let t0;
32 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
@@ -39,13 +39,12 @@ function Component(props) {
39 }
40 const onChange = t0;
41 let t1;
42 - if ($[1] !== x || $[2] !== onChange) {
42 + if ($[1] !== x) {
43 t1 = <input value={x} onChange={onChange} />;
44 $[1] = x;
45 - $[2] = onChange;
46 - $[3] = t1;
45 + $[2] = t1;
46 } else {
48 - t1 = $[3];
47 + t1 = $[2];
48 }
49 return t1;
50 }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactive-control-dependency-phi-setState-type.expect.md new
+111
@@ -0,0 +1,111 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import invariant from "invariant";
6 +import { useState } from "react";
7 +
8 +function Component(props) {
9 + const [x, setX] = useState(false);
10 + const [y, setY] = useState(false);
11 + let setState;
12 + if (props.cond) {
13 + setState = setX;
14 + } else {
15 + setState = setY;
16 + }
17 + const setState2 = setState;
18 + const stateObject = { setState: setState2 };
19 + return (
20 + <Foo
21 + cond={props.cond}
22 + setX={setX}
23 + setY={setY}
24 + setState={stateObject.setState}
25 + />
26 + );
27 +}
28 +
29 +function Foo({ cond, setX, setY, setState }) {
30 + if (cond) {
31 + invariant(setState === setX, "Expected the correct setState function");
32 + } else {
33 + invariant(setState === setY, "Expected the correct setState function");
34 + }
35 + return "ok";
36 +}
37 +
38 +export const FIXTURE_ENTRYPOINT = {
39 + fn: Component,
40 + // TODO: run this function with {cond:true}, {cond: false}
41 + params: [{ cond: true }],
42 +};
43 +
44 +```
45 +
46 +## Code
47 +
48 +```javascript
49 +import invariant from "invariant";
50 +import { useState, unstable_useMemoCache as useMemoCache } from "react";
51 +
52 +function Component(props) {
53 + const $ = useMemoCache(5);
54 + const [x, setX] = useState(false);
55 + const [y, setY] = useState(false);
56 + let setState;
57 + if (props.cond) {
58 + setState = setX;
59 + } else {
60 + setState = setY;
61 + }
62 +
63 + const setState2 = setState;
64 + let t0;
65 + if ($[0] !== setState2) {
66 + t0 = { setState: setState2 };
67 + $[0] = setState2;
68 + $[1] = t0;
69 + } else {
70 + t0 = $[1];
71 + }
72 + const stateObject = t0;
73 + let t1;
74 + if ($[2] !== props.cond || $[3] !== stateObject.setState) {
75 + t1 = (
76 + <Foo
77 + cond={props.cond}
78 + setX={setX}
79 + setY={setY}
80 + setState={stateObject.setState}
81 + />
82 + );
83 + $[2] = props.cond;
84 + $[3] = stateObject.setState;
85 + $[4] = t1;
86 + } else {
87 + t1 = $[4];
88 + }
89 + return t1;
90 +}
91 +
92 +function Foo(t21) {
93 + const { cond, setX, setY, setState } = t21;
94 + if (cond) {
95 + invariant(setState === setX, "Expected the correct setState function");
96 + } else {
97 + invariant(setState === setY, "Expected the correct setState function");
98 + }
99 + return "ok";
100 +}
101 +
102 +export const FIXTURE_ENTRYPOINT = {
103 + fn: Component,
104 + // TODO: run this function with {cond:true}, {cond: false}
105 + params: [{ cond: true }],
106 +};
107 +
108 +```
109 +
110 +### Eval output
111 +(kind: ok) ok
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/reactive-control-dependency-phi-setState-type.js new
+38
@@ -0,0 +1,38 @@
1 +import invariant from "invariant";
2 +import { useState } from "react";
3 +
4 +function Component(props) {
5 + const [x, setX] = useState(false);
6 + const [y, setY] = useState(false);
7 + let setState;
8 + if (props.cond) {
9 + setState = setX;
10 + } else {
11 + setState = setY;
12 + }
13 + const setState2 = setState;
14 + const stateObject = { setState: setState2 };
15 + return (
16 + <Foo
17 + cond={props.cond}
18 + setX={setX}
19 + setY={setY}
20 + setState={stateObject.setState}
21 + />
22 + );
23 +}
24 +
25 +function Foo({ cond, setX, setY, setState }) {
26 + if (cond) {
27 + invariant(setState === setX, "Expected the correct setState function");
28 + } else {
29 + invariant(setState === setY, "Expected the correct setState function");
30 + }
31 + return "ok";
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Component,
36 + // TODO: run this function with {cond:true}, {cond: false}
37 + params: [{ cond: true }],
38 +};