@samitouri / QOS-React / commits / 19e44a4a69

Allow ref argument to be mutated

Previously, Forget would throw if _any_ of the arguments to a component are modified. This isn't quite right as a ref argument can be modified. This PR assumes the second argument of a component to be a ref and allows it to be mutable. A future PR will add types to this argument so the validateRefAccessDuringRender can catch if ref is mutated in render. This PR contains a todo test for this.

Sathya Gunasekaran committed Feb 29, 2024 at 14:47 UTC 19e44a4a69c87ab0fef540f06fa5e65ca0380055
13 files changed +400 -28
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+70 -17
@@ -21,6 +21,7 @@ import {
21 MethodCall,
22 Phi,
23 Place,
24 + SpreadPattern,
25 Type,
26 ValueKind,
27 ValueReason,
@@ -133,26 +134,48 @@ export default function inferReferenceEffects(
134 kind: ValueKind.Frozen,
135 reason: new Set([ValueReason.ReactiveFunctionArgument]),
136 };
136 - for (const param of fn.params) {
137 +
138 + const isComponent = isComponentName(fn.id);
139 + if (isComponent) {
140 + CompilerError.invariant(fn.params.length <= 2, {
141 + reason:
142 + "Expected React component to have not more than two parameters: one for props and for ref",
143 + description: null,
144 + loc: fn.loc,
145 + suggestions: null,
146 + });
147 + const [props, ref] = fn.params;
148 let value: InstructionValue;
149 let place: Place;
139 - if (param.kind === "Identifier") {
140 - place = param;
141 - value = {
142 - kind: "Primitive",
143 - loc: param.loc,
144 - value: undefined,
145 - };
146 - } else {
147 - place = param.place;
148 - value = {
149 - kind: "Primitive",
150 - loc: param.place.loc,
151 - value: undefined,
152 - };
150 + if (props) {
151 + inferParam(props, initialState, paramKind);
152 + }
153 + if (ref) {
154 + if (ref.kind === "Identifier") {
155 + place = ref;
156 + value = {
157 + kind: "ObjectExpression",
158 + properties: [],
159 + loc: ref.loc,
160 + };
161 + } else {
162 + place = ref.place;
163 + value = {
164 + kind: "ObjectExpression",
165 + properties: [],
166 + loc: ref.place.loc,
167 + };
168 + }
169 + initialState.initialize(value, {
170 + kind: ValueKind.Mutable,
171 + reason: new Set([ValueReason.Other]),
172 + });
173 + initialState.define(place, value);
174 + }
175 + } else {
176 + for (const param of fn.params) {
177 + inferParam(param, initialState, paramKind);
178 }
154 - initialState.initialize(value, paramKind);
155 - initialState.define(place, value);
179 }
180
181 // Map of blocks to the last (merged) incoming state that was processed
@@ -596,6 +619,36 @@ class InferenceState {
619 }
620 }
621
622 +function isComponentName(name: string | null): boolean {
623 + return name !== null && /^[A-Z]/.test(name);
624 +}
625 +
626 +function inferParam(
627 + param: Place | SpreadPattern,
628 + initialState: InferenceState,
629 + paramKind: AbstractValue
630 +): void {
631 + let value: InstructionValue;
632 + let place: Place;
633 + if (param.kind === "Identifier") {
634 + place = param;
635 + value = {
636 + kind: "Primitive",
637 + loc: param.loc,
638 + value: undefined,
639 + };
640 + } else {
641 + place = param.place;
642 + value = {
643 + kind: "Primitive",
644 + loc: param.place.loc,
645 + value: undefined,
646 + };
647 + }
648 + initialState.initialize(value, paramKind);
649 + initialState.define(place, value);
650 +}
651 +
652 /*
653 * Joins two values using the following rules:
654 * == Effect Transitions ==
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useEffect } from "react";
6 +
7 +function Foo(props, ref) {
8 + useEffect(() => {
9 + ref.current = 2;
10 + }, []);
11 + return <div>{props.bar}</div>;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Foo,
16 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
17 + isComponent: true,
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { useEffect, unstable_useMemoCache as useMemoCache } from "react";
26 +
27 +function Foo(props, ref) {
28 + const $ = useMemoCache(5);
29 + let t0;
30 + if ($[0] !== ref.current) {
31 + t0 = () => {
32 + ref.current = 2;
33 + };
34 + $[0] = ref.current;
35 + $[1] = t0;
36 + } else {
37 + t0 = $[1];
38 + }
39 + let t1;
40 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
41 + t1 = [];
42 + $[2] = t1;
43 + } else {
44 + t1 = $[2];
45 + }
46 + useEffect(t0, t1);
47 + let t2;
48 + if ($[3] !== props.bar) {
49 + t2 = <div>{props.bar}</div>;
50 + $[3] = props.bar;
51 + $[4] = t2;
52 + } else {
53 + t2 = $[4];
54 + }
55 + return t2;
56 +}
57 +
58 +export const FIXTURE_ENTRYPOINT = {
59 + fn: Foo,
60 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
61 + isComponent: true,
62 +};
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: ok) <div>foo</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.js new
+14
@@ -0,0 +1,14 @@
1 +import { useEffect } from "react";
2 +
3 +function Foo(props, ref) {
4 + useEffect(() => {
5 + ref.current = 2;
6 + }, []);
7 + return <div>{props.bar}</div>;
8 +}
9 +
10 +export const FIXTURE_ENTRYPOINT = {
11 + fn: Foo,
12 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
13 + isComponent: true,
14 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/todo.validate-mutate-ref-arg-in-render.expect.md new
+46
@@ -0,0 +1,46 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender: true
6 +function Foo(props, ref) {
7 + ref.current = 2;
8 + return <div>{props.bar}</div>;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Foo,
13 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
14 + isComponent: true,
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender: true
23 +function Foo(props, ref) {
24 + const $ = useMemoCache(2);
25 + ref.current = 2;
26 + let t0;
27 + if ($[0] !== props.bar) {
28 + t0 = <div>{props.bar}</div>;
29 + $[0] = props.bar;
30 + $[1] = t0;
31 + } else {
32 + t0 = $[1];
33 + }
34 + return t0;
35 +}
36 +
37 +export const FIXTURE_ENTRYPOINT = {
38 + fn: Foo,
39 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
40 + isComponent: true,
41 +};
42 +
43 +```
44 +
45 +### Eval output
46 +(kind: ok) <div>foo</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/todo.validate-mutate-ref-arg-in-render.js new
+11
@@ -0,0 +1,11 @@
1 +// @validateRefAccessDuringRender: true
2 +function Foo(props, ref) {
3 + ref.current = 2;
4 + return <div>{props.bar}</div>;
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Foo,
9 + params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
10 + isComponent: true,
11 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-1.expect.md renamed
+8 -8
@@ -2,8 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @debug
6 -function Component(a, [b], { c }) {
5 +function Component({ a: a, b: [b], c: { c } }) {
6 let d = a++;
7 let e = ++a;
8 let f = b--;
@@ -15,7 +14,7 @@ function Component(a, [b], { c }) {
14
15 export const FIXTURE_ENTRYPOINT = {
16 fn: Component,
18 - params: [2, [3], { c: 4 }],
17 + params: [{ a: 2, b: [3], c: { c: 4 } }],
18 isComponent: false,
19 };
20
@@ -24,11 +23,12 @@ export const FIXTURE_ENTRYPOINT = {
23 ## Code
24
25 ```javascript
27 -import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
28 -function Component(a, t37, t38) {
26 +import { unstable_useMemoCache as useMemoCache } from "react";
27 +function Component(t38) {
28 const $ = useMemoCache(10);
30 - let [b] = t37;
31 - let { c } = t38;
29 + let { a, b: t40, c: t41 } = t38;
30 + let [b] = t40;
31 + let { c } = t41;
32 const d = a++;
33 const e = ++a;
34 const f = b--;
@@ -66,7 +66,7 @@ function Component(a, t37, t38) {
66
67 export const FIXTURE_ENTRYPOINT = {
68 fn: Component,
69 - params: [2, [3], { c: 4 }],
69 + params: [{ a: 2, b: [3], c: { c: 4 } }],
70 isComponent: false,
71 };
72
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-1.js renamed
+2 -3
@@ -1,5 +1,4 @@
1 -// @debug
2 -function Component(a, [b], { c }) {
1 +function Component({ a: a, b: [b], c: { c } }) {
2 let d = a++;
3 let e = ++a;
4 let f = b--;
@@ -11,6 +10,6 @@ function Component(a, [b], { c }) {
10
11 export const FIXTURE_ENTRYPOINT = {
12 fn: Component,
14 - params: [2, [3], { c: 4 }],
13 + params: [{ a: 2, b: [3], c: { c: 4 } }],
14 isComponent: false,
15 };
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-2.expect.md new
+49
@@ -0,0 +1,49 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(a) {
6 + let d = a++;
7 + let e = ++a;
8 + return [a, d, e];
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [2],
14 + isComponent: false,
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { unstable_useMemoCache as useMemoCache } from "react";
23 +function Component(a) {
24 + const $ = useMemoCache(4);
25 + const d = a++;
26 + const e = ++a;
27 + let t0;
28 + if ($[0] !== a || $[1] !== d || $[2] !== e) {
29 + t0 = [a, d, e];
30 + $[0] = a;
31 + $[1] = d;
32 + $[2] = e;
33 + $[3] = t0;
34 + } else {
35 + t0 = $[3];
36 + }
37 + return t0;
38 +}
39 +
40 +export const FIXTURE_ENTRYPOINT = {
41 + fn: Component,
42 + params: [2],
43 + isComponent: false,
44 +};
45 +
46 +```
47 +
48 +### Eval output
49 +(kind: ok) [4,2,4]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-2.js new
+11
@@ -0,0 +1,11 @@
1 +function Component(a) {
2 + let d = a++;
3 + let e = ++a;
4 + return [a, d, e];
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Component,
9 + params: [2],
10 + isComponent: false,
11 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-3.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({ c }) {
6 + let h = c++;
7 + let i = --c;
8 + return [c, h, i];
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [{ c: 4 }],
14 + isComponent: false,
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { unstable_useMemoCache as useMemoCache } from "react";
23 +function Component(t14) {
24 + const $ = useMemoCache(4);
25 + let { c } = t14;
26 + const h = c++;
27 + const i = --c;
28 + let t0;
29 + if ($[0] !== c || $[1] !== h || $[2] !== i) {
30 + t0 = [c, h, i];
31 + $[0] = c;
32 + $[1] = h;
33 + $[2] = i;
34 + $[3] = t0;
35 + } else {
36 + t0 = $[3];
37 + }
38 + return t0;
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{ c: 4 }],
44 + isComponent: false,
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) [4,4,4]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-3.js new
+11
@@ -0,0 +1,11 @@
1 +function Component({ c }) {
2 + let h = c++;
3 + let i = --c;
4 + return [c, h, i];
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Component,
9 + params: [{ c: 4 }],
10 + isComponent: false,
11 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-4.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component([b]) {
6 + let f = b--;
7 + let g = --b;
8 + return [b, f, g];
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [[3]],
14 + isComponent: false,
15 +};
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { unstable_useMemoCache as useMemoCache } from "react";
23 +function Component(t14) {
24 + const $ = useMemoCache(4);
25 + let [b] = t14;
26 + const f = b--;
27 + const g = --b;
28 + let t0;
29 + if ($[0] !== b || $[1] !== f || $[2] !== g) {
30 + t0 = [b, f, g];
31 + $[0] = b;
32 + $[1] = f;
33 + $[2] = g;
34 + $[3] = t0;
35 + } else {
36 + t0 = $[3];
37 + }
38 + return t0;
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [[3]],
44 + isComponent: false,
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) [1,3,1]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-4.js new
+11
@@ -0,0 +1,11 @@
1 +function Component([b]) {
2 + let f = b--;
3 + let g = --b;
4 + return [b, f, g];
5 +}
6 +
7 +export const FIXTURE_ENTRYPOINT = {
8 + fn: Component,
9 + params: [[3]],
10 + isComponent: false,
11 +};