@samitouri / QOS-React-2 / commits / 04f4b50997

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