@samitouri / QOS-React / commits / 7f60f32118

Dont memoize scopes with hook calls

Joe Savona committed Feb 8, 2023 at 14:07 UTC 7f60f321186ae64001adff905fdc1403aee327f2
12 files changed +189 -44
compiler/forget/src/CompilerPipeline.ts
+8
@@ -33,6 +33,7 @@ import {
33 pruneUnusedScopes,
34 renameVariables,
35 } from "./ReactiveScopes";
36 +import { flattenScopesWithHooks } from "./ReactiveScopes/FlattenScopesWithHooks";
37 import { eliminateRedundantPhi, enterSSA, leaveSSA } from "./SSA";
38 import { inferTypes } from "./TypeInference";
39 import { logHIRFunction, logReactiveFunction } from "./Utils/logger";
@@ -118,6 +119,13 @@ export function* run(
119 value: reactiveFunction,
120 });
121
122 + flattenScopesWithHooks(reactiveFunction);
123 + yield log({
124 + kind: "reactive",
125 + name: "FlattenScopesWithHooks",
126 + value: reactiveFunction,
127 + });
128 +
129 propagateScopeDependencies(reactiveFunction);
130 yield log({
131 kind: "reactive",
compiler/forget/src/Inference/InferReferenceEffects.ts
+1 -1
@@ -801,7 +801,7 @@ const HOOKS: Map<string, Hook> = new Map([
801 type HookKind = { kind: "State" } | { kind: "Ref" } | { kind: "Custom" };
802 type Hook = HookKind & { effectKind: Effect; valueKind: ValueKind };
803
804 -function parseHookCall(place: Place): Hook | null {
804 +export function parseHookCall(place: Place): Hook | null {
805 const name = place.identifier.name;
806 if (name === null || !name.match(/^_?use/)) {
807 return null;
compiler/forget/src/ReactiveScopes/FlattenScopesWithHooks.ts new
+66
@@ -0,0 +1,66 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 { parseHookCall } from "../Inference/InferReferenceEffects";
16 +import {
17 + ReactiveFunctionTransform,
18 + Transformed,
19 + visitReactiveFunction,
20 +} from "./visitors";
21 +
22 +/**
23 + * Most parts of compilation do not treat hooks specially, because there is no guarantee that custom
24 + * hooks obey any particular contract. For example, we can't assume that custom hooks won't modify
25 + * their arguments, and we can't assume that hooks return immutable or memoized values. Therefore
26 + * earlier passes largely ignore hooks, and may end up creating reactive scopes that contain hook calls.
27 + *
28 + * This pass then finds and removes any scopes that transitively contain a hook call. By running all
29 + * the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
30 + * describe the set of values which "construct together", and remove _all_ that memoization in order
31 + * to ensure the hook call does not inadvertently become conditional.
32 + */
33 +export function flattenScopesWithHooks(fn: ReactiveFunction): void {
34 + visitReactiveFunction(fn, new Transform(), { hasHook: false });
35 +}
36 +
37 +type State = { hasHook: boolean };
38 +
39 +class Transform extends ReactiveFunctionTransform<State> {
40 + override transformScope(
41 + scope: ReactiveScopeBlock,
42 + outerState: State
43 + ): Transformed<ReactiveStatement> {
44 + const innerState: State = { hasHook: false };
45 + this.visitScope(scope, innerState);
46 + outerState.hasHook ||= innerState.hasHook;
47 + if (innerState.hasHook) {
48 + return { kind: "replace-many", value: scope.instructions };
49 + } else {
50 + return { kind: "keep" };
51 + }
52 + }
53 +
54 + override visitValue(
55 + id: InstructionId,
56 + value: ReactiveValue,
57 + state: State
58 + ): void {
59 + if (value.kind === "CallExpression") {
60 + const hook = parseHookCall(value.callee);
61 + if (hook !== null) {
62 + state.hasHook = true;
63 + }
64 + }
65 + }
66 +}
compiler/forget/src/ReactiveScopes/InferReactiveIdentifiers.ts
+12
@@ -12,6 +12,7 @@ import {
12 ReactiveInstruction,
13 ReactiveScope,
14 } from "../HIR/HIR";
15 +import { parseHookCall } from "../Inference/InferReferenceEffects";
16 import {
17 eachReactiveValueOperand,
18 ReactiveFunctionVisitor,
@@ -40,6 +41,17 @@ class Environment extends ReactiveFunctionVisitor<IdentifierReactivity> {
41 break;
42 }
43 }
44 + if (!hasReactiveInput && instr.value.kind === "CallExpression") {
45 + // Hooks cannot be memoized. Even if they do not accept any reactive inputs,
46 + // they are not guaranteed to memoize their return value, and their result
47 + // must be assumed to be reactive.
48 + // TODO: use types or an opt-in registry of custom hook information to
49 + // allow treating safe hooks as non-reactive.
50 + const hook = parseHookCall(instr.value.callee);
51 + if (hook !== null) {
52 + hasReactiveInput = true;
53 + }
54 + }
55 reactivityMap.set(lval.place.identifier, hasReactiveInput);
56
57 if (hasReactiveInput) {
compiler/forget/src/__tests__/fixtures/hir/concise-arrow-expr.expect.md
+12 -15
@@ -15,29 +15,26 @@ function component() {
15 ```javascript
16 function component() {
17 const $ = React.unstable_useMemoCache();
18 - let t0;
19 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
20 - t0 = useState(0);
21 - $[0] = t0;
22 - } else {
23 - t0 = $[0];
24 - }
25 - const setX = t0[1];
18 + const setX = useState(0)[1];
19 + const c_0 = $[0] !== setX;
20 let handler;
27 - if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
21 + if (c_0) {
22 handler = (v) => setX(v);
23 + $[0] = setX;
24 $[1] = handler;
25 } else {
26 handler = $[1];
27 }
33 - let t1;
34 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
35 - t1 = <Foo handler={handler}></Foo>;
36 - $[2] = t1;
28 + const c_2 = $[2] !== handler;
29 + let t0;
30 + if (c_2) {
31 + t0 = <Foo handler={handler}></Foo>;
32 + $[2] = handler;
33 + $[3] = t0;
34 } else {
38 - t1 = $[2];
35 + t0 = $[3];
36 }
40 - return t1;
37 + return t0;
38 }
39
40 ```
compiler/forget/src/__tests__/fixtures/hir/controlled-input.expect.md new
+44
@@ -0,0 +1,44 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function component() {
6 + let [x, setX] = useState(0);
7 + const handler = (event) => setX(event.target.value);
8 + return <input onChange={handler} value={x} />;
9 +}
10 +
11 +```
12 +
13 +## Code
14 +
15 +```javascript
16 +function component() {
17 + const $ = React.unstable_useMemoCache();
18 + const x = useState(0)[0];
19 + const setX = useState(0)[1];
20 + const c_0 = $[0] !== setX;
21 + let handler;
22 + if (c_0) {
23 + handler = (event) => setX(event.target.value);
24 + $[0] = setX;
25 + $[1] = handler;
26 + } else {
27 + handler = $[1];
28 + }
29 + const c_2 = $[2] !== handler;
30 + const c_3 = $[3] !== x;
31 + let t0;
32 + if (c_2 || c_3) {
33 + t0 = <input onChange={handler} value={x}></input>;
34 + $[2] = handler;
35 + $[3] = x;
36 + $[4] = t0;
37 + } else {
38 + t0 = $[4];
39 + }
40 + return t0;
41 +}
42 +
43 +```
44 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/controlled-input.js new
+5
@@ -0,0 +1,5 @@
1 +function component() {
2 + let [x, setX] = useState(0);
3 + const handler = (event) => setX(event.target.value);
4 + return <input onChange={handler} value={x} />;
5 +}
compiler/forget/src/__tests__/fixtures/hir/hook-call.expect.md
+4 -8
@@ -34,22 +34,18 @@ function Component(props) {
34 } else {
35 x = $[0];
36 }
37 - let y;
38 - if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
39 - y = useFreeze(x);
40 - $[1] = y;
41 - } else {
42 - y = $[1];
43 - }
37 + const y = useFreeze(x);
38 foo(y, x);
39 + const c_1 = $[1] !== y;
40 let t0;
46 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
41 + if (c_1) {
42 t0 = (
43 <Component>
44 {x}
45 {y}
46 </Component>
47 );
48 + $[1] = y;
49 $[2] = t0;
50 } else {
51 t0 = $[2];
compiler/forget/src/__tests__/fixtures/hir/nested-scopes-hook-call.expect.md new
+27
@@ -0,0 +1,27 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function component(props) {
6 + let x = [];
7 + let y = [];
8 + y.push(useHook(props.foo));
9 + x.push(y);
10 + return x;
11 +}
12 +
13 +```
14 +
15 +## Code
16 +
17 +```javascript
18 +function component(props) {
19 + const x = [];
20 + const y = [];
21 + y.push(useHook(props.foo));
22 + x.push(y);
23 + return x;
24 +}
25 +
26 +```
27 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/hir/nested-scopes-hook-call.js new
+7
@@ -0,0 +1,7 @@
1 +function component(props) {
2 + let x = [];
3 + let y = [];
4 + y.push(useHook(props.foo));
5 + x.push(y);
6 + return x;
7 +}
compiler/forget/src/__tests__/fixtures/hir/optional-member-expression.expect.md
+2 -9
@@ -27,15 +27,8 @@ function Foo(props) {
27 x = $[1];
28 }
29 const y = x?.b;
30 - const c_2 = $[2] !== y;
31 - let z;
32 - if (c_2) {
33 - z = useBar(y);
34 - $[2] = y;
35 - $[3] = z;
36 - } else {
37 - z = $[3];
38 - }
30 +
31 + const z = useBar(y);
32 return z;
33 }
34
compiler/forget/src/__tests__/fixtures/hir/template-literal.expect.md
+1 -11
@@ -25,17 +25,7 @@ function componentA(props) {
25 }
26
27 function componentB(props) {
28 - const $ = React.unstable_useMemoCache();
29 - const t0 = `hello ${props.a}`;
30 - const c_0 = $[0] !== t0;
31 - let x;
32 - if (c_0) {
33 - x = useFoo(t0);
34 - $[0] = t0;
35 - $[1] = x;
36 - } else {
37 - x = $[1];
38 - }
28 + const x = useFoo(`hello ${props.a}`);
29 return x;
30 }
31