[validation] Reduce false positives in validatePreserveExistingMemo
--- Previously (in #2663), we check that inferred dependencies exactly match source dependencies. As @validatePreserveExistingMemoizationGuarantees checks that Forget output does not invalidate *any more* than source, we now allow more specific inferred dependencies when neither the inferred dep or matching source dependency reads into a ref (based on `.current` access). See added comments for more details
Mofei Zhang committed
Mar 20, 2024 at 16:05 UTC
63bb781781ef1b4b08ee169b43f08dece4232a39
9 files changed
+198
-96
compiler/packages/babel-plugin-react-forget/src/Validation/ValidatePreservedManualMemoization.ts
+55
-23
@@ -101,24 +101,59 @@ function prettyPrintScopeDependency(val: ReactiveScopeDependency): string {
101
}
102
return `${rootStr}${val.path.length > 0 ? "." : ""}${val.path.join(".")}`;
103
}
104
-function depsEqual(
105
- dep1: ManualMemoDependency,
106
- dep2: ManualMemoDependency
104
+
105
+function compareDeps(
106
+ inferred: ManualMemoDependency,
107
+ source: ManualMemoDependency
108
): boolean {
109
const rootsEqual =
109
- (dep1.root.kind === "Global" &&
110
- dep2.root.kind === "Global" &&
111
- dep1.root.identifierName === dep2.root.identifierName) ||
112
- (dep1.root.kind === "NamedLocal" &&
113
- dep2.root.kind === "NamedLocal" &&
114
- dep1.root.value.identifier.id === dep2.root.value.identifier.id);
115
- return (
116
- rootsEqual &&
117
- dep1.path.length === dep2.path.length &&
118
- dep1.path.every((val, idx) => val === dep2.path[idx])
119
- );
110
+ (inferred.root.kind === "Global" &&
111
+ source.root.kind === "Global" &&
112
+ inferred.root.identifierName === source.root.identifierName) ||
113
+ (inferred.root.kind === "NamedLocal" &&
114
+ source.root.kind === "NamedLocal" &&
115
+ inferred.root.value.identifier.id === source.root.value.identifier.id);
116
+ if (!rootsEqual) {
117
+ return false;
118
+ }
119
+
120
+ let isSubpath = true;
121
+ for (let i = 0; i < Math.min(inferred.path.length, source.path.length); i++) {
122
+ if (inferred.path[i] !== source.path[i]) {
123
+ isSubpath = false;
124
+ break;
125
+ }
126
+ }
127
+
128
+ if (
129
+ isSubpath &&
130
+ (source.path.length === inferred.path.length ||
131
+ (inferred.path.length >= source.path.length &&
132
+ !inferred.path.includes("current")))
133
+ ) {
134
+ return true;
135
+ } else {
136
+ return false;
137
+ }
138
}
139
140
+/**
141
+ * Validate that an inferred dependency either matches a source dependency
142
+ * or is produced by earlier instructions in the same manual memoization
143
+ * call.
144
+ * Inferred dependency `rootA.[pathA]` matches a source dependency `rootB.[pathB]`
145
+ * when:
146
+ * - rootA and rootB are loads from the same named identifier. Note that this
147
+ * identifier must be also named in source, as DropManualMemoization, which
148
+ * runs before any renaming passes, only records loads from named variables.
149
+ * - and one of the following holds:
150
+ * - pathA and pathB are identifical
151
+ * - pathB is a subpath of pathA and neither read into a `ref` type*
152
+ *
153
+ * We do not allow for partial matches on ref types because they are not immutable
154
+ * values, e.g.
155
+ * ref_prev === ref_new does not imply ref_prev.current === ref_new.current
156
+ */
157
function validateInferredDep(
158
dep: ReactiveScopeDependency,
159
temporaries: Map<IdentifierId, ManualMemoDependency>,
@@ -154,22 +189,19 @@ function validateInferredDep(
189
path: [...dep.path],
190
};
191
}
157
- for (const originalDep of validDepsInMemoBlock) {
158
- if (depsEqual(originalDep, normalizedDep)) {
159
- return;
160
- }
161
- }
192
for (const decl of declsWithinMemoBlock) {
163
- const normalizedDecl = temporaries.get(decl);
164
- if (normalizedDecl != null && depsEqual(normalizedDecl, normalizedDep)) {
165
- return;
166
- } else if (
193
+ if (
194
normalizedDep.root.kind === "NamedLocal" &&
195
decl === normalizedDep.root.value.identifier.id
196
) {
197
return;
198
}
199
}
200
+ for (const originalDep of validDepsInMemoBlock) {
201
+ if (compareDeps(normalizedDep, originalDep)) {
202
+ return;
203
+ }
204
+ }
205
errorState.push({
206
severity: ErrorSeverity.Todo,
207
reason:
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useCallback-infer-more-specific.expect.md
deleted
-28
@@ -1,28 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @validatePreserveExistingMemoizationGuarantees
6
-
7
-import { useCallback } from "react";
8
-
9
-// False positive as more specific memoization always results
10
-// in fewer memo block executions.
11
-// Precisely:
12
-// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
13
-// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14
-// One fix would be to depend on optional chains
15
-function useHook(x) {
16
- return useCallback(() => [x.y.z], [x]);
17
-}
18
-
19
-```
20
-
21
-
22
-## Error
23
-
24
-```
25
-[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x]
26
-```
27
-
28
-
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.expect.md
deleted
-27
@@ -1,27 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @validatePreserveExistingMemoizationGuarantees
6
-
7
-import { useMemo } from "react";
8
-
9
-// False positive as more specific memoization always results
10
-// in fewer memo block executions.
11
-// Precisely:
12
-// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
13
-// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14
-function useHook(x) {
15
- return useMemo(() => [x.y.z], [x]);
16
-}
17
-
18
-```
19
-
20
-
21
-## Error
22
-
23
-```
24
-[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x]
25
-```
26
-
27
-
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.false-positive-useMemo-infer-more-specific.ts
deleted
-12
@@ -1,12 +0,0 @@
1
-// @validatePreserveExistingMemoizationGuarantees
2
-
3
-import { useMemo } from "react";
4
-
5
-// False positive as more specific memoization always results
6
-// in fewer memo block executions.
7
-// Precisely:
8
-// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
9
-// x.y.z_new != x.y.z_prev does imply x_new != x_prev
10
-function useHook(x) {
11
- return useMemo(() => [x.y.z], [x]);
12
-}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useCallback-aliased-var.expect.md
-2
@@ -20,8 +20,6 @@ function useHook(x) {
20
21
```
22
[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `aliasedX`, but the source dependencies were [x, aliasedProp]
23
-
24
-[ReactForget] Todo: Could not preserve manual memoization because an inferred dependency does not match the dependency list in source. The inferred dependency was `x.y.z`, but the source dependencies were [x, aliasedProp]
23
```
24
25
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.expect.md
new
+58
@@ -0,0 +1,58 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validatePreserveExistingMemoizationGuarantees
6
+
7
+import { useCallback } from "react";
8
+
9
+// More specific memoization always results in fewer memo block
10
+// executions.
11
+// Precisely:
12
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
13
+// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14
+function useHook(x) {
15
+ return useCallback(() => [x.y.z], [x]);
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: useHook,
20
+ params: [{ y: { z: 2 } }],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+// @validatePreserveExistingMemoizationGuarantees
29
+
30
+import { useCallback, unstable_useMemoCache as useMemoCache } from "react";
31
+
32
+// More specific memoization always results in fewer memo block
33
+// executions.
34
+// Precisely:
35
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
36
+// x.y.z_new != x.y.z_prev does imply x_new != x_prev
37
+function useHook(x) {
38
+ const $ = useMemoCache(2);
39
+ let t0;
40
+ if ($[0] !== x.y.z) {
41
+ t0 = () => [x.y.z];
42
+ $[0] = x.y.z;
43
+ $[1] = t0;
44
+ } else {
45
+ t0 = $[1];
46
+ }
47
+ return t0;
48
+}
49
+
50
+export const FIXTURE_ENTRYPOINT = {
51
+ fn: useHook,
52
+ params: [{ y: { z: 2 } }],
53
+};
54
+
55
+```
56
+
57
+### Eval output
58
+(kind: ok) "[[ function params=0 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-more-specific.ts
renamed
+8
-4
@@ -2,12 +2,16 @@
2
3
import { useCallback } from "react";
4
5
-// False positive as more specific memoization always results
6
-// in fewer memo block executions.
5
+// More specific memoization always results in fewer memo block
6
+// executions.
7
// Precisely:
8
-// x_new != x_prev does not imply x.y.z_new != x.y.z_prev
8
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
9
// x.y.z_new != x.y.z_prev does imply x_new != x_prev
10
-// One fix would be to depend on optional chains
10
function useHook(x) {
11
return useCallback(() => [x.y.z], [x]);
12
}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: useHook,
16
+ params: [{ y: { z: 2 } }],
17
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.expect.md
new
+60
@@ -0,0 +1,60 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validatePreserveExistingMemoizationGuarantees
6
+
7
+import { useMemo } from "react";
8
+
9
+// More specific memoization always results in fewer memo block
10
+// executions.
11
+// Precisely:
12
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
13
+// x.y.z_new != x.y.z_prev does imply x_new != x_prev
14
+function useHook(x) {
15
+ return useMemo(() => [x.y.z], [x]);
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: useHook,
20
+ params: [{ y: { z: 2 } }],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+// @validatePreserveExistingMemoizationGuarantees
29
+
30
+import { useMemo, unstable_useMemoCache as useMemoCache } from "react";
31
+
32
+// More specific memoization always results in fewer memo block
33
+// executions.
34
+// Precisely:
35
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
36
+// x.y.z_new != x.y.z_prev does imply x_new != x_prev
37
+function useHook(x) {
38
+ const $ = useMemoCache(2);
39
+ let t0;
40
+ let t1;
41
+ if ($[0] !== x.y.z) {
42
+ t1 = [x.y.z];
43
+ $[0] = x.y.z;
44
+ $[1] = t1;
45
+ } else {
46
+ t1 = $[1];
47
+ }
48
+ t0 = t1;
49
+ return t0;
50
+}
51
+
52
+export const FIXTURE_ENTRYPOINT = {
53
+ fn: useHook,
54
+ params: [{ y: { z: 2 } }],
55
+};
56
+
57
+```
58
+
59
+### Eval output
60
+(kind: ok) [2]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.ts
new
+17
@@ -0,0 +1,17 @@
1
+// @validatePreserveExistingMemoizationGuarantees
2
+
3
+import { useMemo } from "react";
4
+
5
+// More specific memoization always results in fewer memo block
6
+// executions.
7
+// Precisely:
8
+// x_new != x_prev does NOT imply x.y.z_new != x.y.z_prev
9
+// x.y.z_new != x.y.z_prev does imply x_new != x_prev
10
+function useHook(x) {
11
+ return useMemo(() => [x.y.z], [x]);
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: useHook,
16
+ params: [{ y: { z: 2 } }],
17
+};