@samitouri / QOS-React-2 / commits / 63bb781781

[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 +};