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