Integrate mutable context identifier inference, improve frozen function inference
Integrate mutable context identifier inference, improve frozen function inference Integrates the new inference to detect definitive mutations of context variables. This allows us to more precisely infer mutable function expressions and validate that they aren't passed where frozen values are expected.
Joe Savona committed
Jun 16, 2023 at 14:42 UTC
04f4b50997f347c2c7793274872cb9e70828905d
13 files changed
+197
-132
compiler/forget/packages/babel-plugin-react-forget/src/HIR/ValidateFrozenLambdas.ts
+1
-2
@@ -16,7 +16,6 @@ import {
16
HIRFunction,
17
IdentifierId,
18
Place,
19
- isMutableEffect,
19
isRefValueType,
20
isUseRefType,
21
} from "./HIR";
@@ -106,7 +105,7 @@ function validateOperand(
105
lambda !== undefined &&
106
lambda.dependencies.some(
107
(place) =>
109
- isMutableEffect(place.effect, place.loc) &&
108
+ place.effect === Effect.Mutate &&
109
!isRefValueType(place.identifier) &&
110
!isUseRefType(place.identifier)
111
)
compiler/forget/packages/babel-plugin-react-forget/src/Inference/AnalyseFunctions.ts
+23
-11
@@ -18,9 +18,11 @@ import {
18
ReactiveScopeDependency,
19
} from "../HIR";
20
import { constantPropagation } from "../Optimization";
21
-import { eliminateRedundantPhi, enterSSA } from "../SSA";
21
+import { inferReactiveScopeVariables } from "../ReactiveScopes";
22
+import { eliminateRedundantPhi, enterSSA, leaveSSA } from "../SSA";
23
import { inferTypes } from "../TypeInference";
24
import { logHIRFunction } from "../Utils/logger";
25
+import { inferMutableContextVariables } from "./InferMutableContextVariables";
26
import { inferMutableRanges } from "./InferMutableRanges";
27
import inferReferenceEffects from "./InferReferenceEffects";
28
@@ -112,6 +114,9 @@ function lower(func: HIRFunction): void {
114
analyseFunctions(func);
115
inferReferenceEffects(func, { isFunctionExpression: true });
116
inferMutableRanges(func);
117
+ leaveSSA(func);
118
+ inferReactiveScopeVariables(func);
119
+ inferMutableContextVariables(func);
120
logHIRFunction("AnalyseFunction (inner)", func);
121
}
122
@@ -120,12 +125,15 @@ function infer(
125
state: IdentifierState,
126
context: Place[]
127
): void {
123
- const mutations = new Set(
124
- value.loweredFunc.context
125
- .filter((dep) => isMutatedOrReassigned(dep.identifier))
126
- .map((m) => m.identifier.name)
127
- .filter((m) => m !== null) as string[]
128
- );
128
+ const mutations = new Map<string, Effect>();
129
+ for (const operand of value.loweredFunc.context) {
130
+ if (
131
+ isMutatedOrReassigned(operand.identifier) &&
132
+ operand.identifier.name !== null
133
+ ) {
134
+ mutations.set(operand.identifier.name, operand.effect);
135
+ }
136
+ }
137
138
for (const dep of value.dependencies) {
139
let name: string | null = null;
@@ -144,8 +152,11 @@ function infer(
152
// could be called, and allows us to help ensure it isn't called during
153
// render
154
dep.effect = Effect.Capture;
147
- } else if (name !== null && mutations.has(name)) {
148
- dep.effect = Effect.Capture;
155
+ } else if (name !== null) {
156
+ const effect = mutations.get(name);
157
+ if (effect !== undefined) {
158
+ dep.effect = effect === Effect.Unknown ? Effect.Capture : effect;
159
+ }
160
}
161
}
162
@@ -161,8 +172,9 @@ function infer(
172
"context refs should always have a name"
173
);
174
164
- if (mutations.has(place.identifier.name)) {
165
- place.effect = Effect.Capture;
175
+ const effect = mutations.get(place.identifier.name);
176
+ if (effect !== undefined) {
177
+ place.effect = effect === Effect.Unknown ? Effect.Capture : effect;
178
value.dependencies.push(place);
179
}
180
}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.expect.md
new
+62
@@ -0,0 +1,62 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @debug
6
+function component(a, b) {
7
+ let z = { a };
8
+ let y = b;
9
+ let x = function () {
10
+ if (y) {
11
+ // we don't know for sure this mutates, so we should assume
12
+ // that there is no mutation so long as `x` isn't called
13
+ // during render
14
+ maybeMutate(z);
15
+ }
16
+ };
17
+ return x;
18
+}
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
26
+function component(a, b) {
27
+ const $ = useMemoCache(5);
28
+ const c_0 = $[0] !== a;
29
+ let t0;
30
+ if (c_0) {
31
+ t0 = { a };
32
+ $[0] = a;
33
+ $[1] = t0;
34
+ } else {
35
+ t0 = $[1];
36
+ }
37
+ const z = t0;
38
+ const y = b;
39
+ const c_2 = $[2] !== y;
40
+ const c_3 = $[3] !== z;
41
+ let t1;
42
+ if (c_2 || c_3) {
43
+ t1 = function () {
44
+ if (y) {
45
+ // we don't know for sure this mutates, so we should assume
46
+ // that there is no mutation so long as `x` isn't called
47
+ // during render
48
+ maybeMutate(z);
49
+ }
50
+ };
51
+ $[2] = y;
52
+ $[3] = z;
53
+ $[4] = t1;
54
+ } else {
55
+ t1 = $[4];
56
+ }
57
+ const x = t1;
58
+ return x;
59
+}
60
+
61
+```
62
+
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.js
renamed
+1
@@ -1,3 +1,4 @@
1
+// @debug
2
function component(a, b) {
3
let z = { a };
4
let y = b;
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-function-expression-mutates-immutable-value.expect.md
new
+24
@@ -0,0 +1,24 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const [x, setX] = useState({ value: "" });
7
+ const onChange = (e) => {
8
+ // INVALID! should use copy-on-write and pass the new value
9
+ x.value = e.target.value;
10
+ setX(x);
11
+ };
12
+ return <input value={x.value} onChange={onChange} />;
13
+}
14
+
15
+```
16
+
17
+
18
+## Error
19
+
20
+```
21
+[ReactForget] InvalidInput: InferReferenceEffects: inferred mutation of known immutable value. Found mutation of $40 (frozen) (5:5)
22
+```
23
+
24
+
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-function-expression-mutates-immutable-value.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-capturing-function-conditional-capture-mutate.expect.md
deleted
-28
@@ -1,28 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function component(a, b) {
6
- let z = { a };
7
- let y = b;
8
- let x = function () {
9
- if (y) {
10
- // we don't know for sure this mutates, so we should assume
11
- // that there is no mutation so long as `x` isn't called
12
- // during render
13
- maybeMutate(z);
14
- }
15
- };
16
- return x;
17
-}
18
-
19
-```
20
-
21
-
22
-## Error
23
-
24
-```
25
-[ReactForget] InvalidInput: Cannot use a mutable function where an immutable value is expected (12:12)
26
-```
27
-
28
-
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-function-expression-captures-value-later-frozen-jsx.expect.md
deleted
-27
@@ -1,27 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function Component(props) {
6
- let x = {};
7
- // onChange should be inferred as immutable, because the value
8
- // it captures (`x`) is frozen by the time the function is referenced
9
- const onChange = (e) => {
10
- maybeMutate(x, e.target.value);
11
- };
12
- if (props.cond) {
13
- <div>{x}</div>;
14
- }
15
- return <Foo value={x} onChange={onChange} />;
16
-}
17
-
18
-```
19
-
20
-
21
-## Error
22
-
23
-```
24
-[ReactForget] InvalidInput: Cannot use a mutable function where an immutable value is expected (11:11)
25
-```
26
-
27
-
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/function-expression-captures-value-later-frozen-jsx.expect.md
new
+57
@@ -0,0 +1,57 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let x = {};
7
+ // onChange should be inferred as immutable, because the value
8
+ // it captures (`x`) is frozen by the time the function is referenced
9
+ const onChange = (e) => {
10
+ maybeMutate(x, e.target.value);
11
+ };
12
+ if (props.cond) {
13
+ <div>{x}</div>;
14
+ }
15
+ return <Foo value={x} onChange={onChange} />;
16
+}
17
+
18
+```
19
+
20
+## Code
21
+
22
+```javascript
23
+import { unstable_useMemoCache as useMemoCache } from "react";
24
+function Component(props) {
25
+ const $ = useMemoCache(3);
26
+ let t0;
27
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
28
+ t0 = {};
29
+ $[0] = t0;
30
+ } else {
31
+ t0 = $[0];
32
+ }
33
+ const x = t0;
34
+ let t1;
35
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
36
+ t1 = (e) => {
37
+ maybeMutate(x, e.target.value);
38
+ };
39
+ $[1] = t1;
40
+ } else {
41
+ t1 = $[1];
42
+ }
43
+ const onChange = t1;
44
+ if (props.cond) {
45
+ }
46
+ let t2;
47
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
48
+ t2 = <Foo value={x} onChange={onChange} />;
49
+ $[2] = t2;
50
+ } else {
51
+ t2 = $[2];
52
+ }
53
+ return t2;
54
+}
55
+
56
+```
57
+
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/function-expression-captures-value-later-frozen-jsx.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/function-expression-maybe-mutates-hook-return-value.expect.md
renamed
+29
-4
@@ -17,11 +17,36 @@ function Component(props) {
17
18
```
19
20
+## Code
21
21
-## Error
22
+```javascript
23
+import { unstable_useMemoCache as useMemoCache } from "react";
24
+function Component(props) {
25
+ const $ = useMemoCache(4);
26
+ const id = useSelectedEntitytId();
27
+ const c_0 = $[0] !== id;
28
+ let t0;
29
+ if (c_0) {
30
+ t0 = () => {
31
+ log(id);
32
+ };
33
+ $[0] = id;
34
+ $[1] = t0;
35
+ } else {
36
+ t0 = $[1];
37
+ }
38
+ const onLoad = t0;
39
+ const c_2 = $[2] !== onLoad;
40
+ let t1;
41
+ if (c_2) {
42
+ t1 = <Foo onLoad={onLoad} />;
43
+ $[2] = onLoad;
44
+ $[3] = t1;
45
+ } else {
46
+ t1 = $[3];
47
+ }
48
+ return t1;
49
+}
50
51
```
24
-[ReactForget] InvalidInput: Cannot use a mutable function where an immutable value is expected (11:11)
25
-```
26
-
52
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/function-expression-maybe-mutates-hook-return-value.js
renamed
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/todo-invalid-function-expression-mutates-immutable-value.expect.md
deleted
-60
@@ -1,60 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function Component(props) {
6
- const [x, setX] = useState({ value: "" });
7
- const onChange = (e) => {
8
- // INVALID! should use copy-on-write and pass the new value
9
- x.value = e.target.value;
10
- setX(x);
11
- };
12
- return <input value={x.value} onChange={onChange} />;
13
-}
14
-
15
-```
16
-
17
-## Code
18
-
19
-```javascript
20
-import { unstable_useMemoCache as useMemoCache } from "react";
21
-function Component(props) {
22
- const $ = useMemoCache(6);
23
- let t0;
24
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25
- t0 = { value: "" };
26
- $[0] = t0;
27
- } else {
28
- t0 = $[0];
29
- }
30
- const [x, setX] = useState(t0);
31
- const c_1 = $[1] !== x;
32
- let t1;
33
- if (c_1) {
34
- t1 = (e) => {
35
- // INVALID! should use copy-on-write and pass the new value
36
- x.value = e.target.value;
37
- setX(x);
38
- };
39
- $[1] = x;
40
- $[2] = t1;
41
- } else {
42
- t1 = $[2];
43
- }
44
- const onChange = t1;
45
- const c_3 = $[3] !== x.value;
46
- const c_4 = $[4] !== onChange;
47
- let t2;
48
- if (c_3 || c_4) {
49
- t2 = <input value={x.value} onChange={onChange} />;
50
- $[3] = x.value;
51
- $[4] = onChange;
52
- $[5] = t2;
53
- } else {
54
- t2 = $[5];
55
- }
56
- return t2;
57
-}
58
-
59
-```
60
-
\ No newline at end of file