[tests] Add fixtures showing HIR rewrite changes
ghstack-source-id: 5e8b1680e95d112e1e5b2cb8c4e6c414360e0a77 Pull Request resolved: https://github.com/facebook/react-forget/pull/2900
Mofei Zhang committed
Apr 29, 2024 at 14:08 UTC
6db5f36aa739169266d385f6f5fb4d585ac055b8
42 files changed
+2476
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/align-scopes-within-nested-valueblock-in-array.expect.md
new
+84
@@ -0,0 +1,84 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+
7
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
8
+
9
+/**
10
+ * Here, identity('foo') is an immutable allocating instruction.
11
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
12
+ *
13
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
14
+ * both scopes to end at value blocks within the <></> expression.
15
+ * However, both scope ranges should be aligned to the outer value block
16
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
17
+ * handles this correctly.
18
+ */
19
+function Foo({ cond1, cond2 }) {
20
+ const arr = makeArray<any>({ a: 2 }, 2, []);
21
+
22
+ return cond1 ? (
23
+ <>
24
+ <div>{identity("foo")}</div>
25
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
26
+ </>
27
+ ) : null;
28
+}
29
+
30
+export const FIXTURE_ENTRYPOINT = {
31
+ fn: Foo,
32
+ params: [{ cond1: true, cond2: true }],
33
+};
34
+
35
+```
36
+
37
+## Code
38
+
39
+```javascript
40
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
41
+
42
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
43
+
44
+/**
45
+ * Here, identity('foo') is an immutable allocating instruction.
46
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
47
+ *
48
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
49
+ * both scopes to end at value blocks within the <></> expression.
50
+ * However, both scope ranges should be aligned to the outer value block
51
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
52
+ * handles this correctly.
53
+ */
54
+function Foo(t0) {
55
+ const $ = useMemoCache(4);
56
+ const { cond1, cond2 } = t0;
57
+ const arr = makeArray({ a: 2 }, 2, []);
58
+ let t1;
59
+ if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== arr) {
60
+ t1 = cond1 ? (
61
+ <>
62
+ <div>{identity("foo")}</div>
63
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
64
+ </>
65
+ ) : null;
66
+ $[0] = cond1;
67
+ $[1] = cond2;
68
+ $[2] = arr;
69
+ $[3] = t1;
70
+ } else {
71
+ t1 = $[3];
72
+ }
73
+ return t1;
74
+}
75
+
76
+export const FIXTURE_ENTRYPOINT = {
77
+ fn: Foo,
78
+ params: [{ cond1: true, cond2: true }],
79
+};
80
+
81
+```
82
+
83
+### Eval output
84
+(kind: ok) <div>foo</div><div>{"value":[null,null,null]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/align-scopes-within-nested-valueblock-in-array.tsx
new
+29
@@ -0,0 +1,29 @@
1
+// @enableReactiveScopesInHIR:false
2
+
3
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
4
+
5
+/**
6
+ * Here, identity('foo') is an immutable allocating instruction.
7
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
8
+ *
9
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
10
+ * both scopes to end at value blocks within the <></> expression.
11
+ * However, both scope ranges should be aligned to the outer value block
12
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
13
+ * handles this correctly.
14
+ */
15
+function Foo({ cond1, cond2 }) {
16
+ const arr = makeArray<any>({ a: 2 }, 2, []);
17
+
18
+ return cond1 ? (
19
+ <>
20
+ <div>{identity("foo")}</div>
21
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
22
+ </>
23
+ ) : null;
24
+}
25
+
26
+export const FIXTURE_ENTRYPOINT = {
27
+ fn: Foo,
28
+ params: [{ cond1: true, cond2: true }],
29
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/allocating-logical-expression-instruction-scope.expect.md
new
+65
@@ -0,0 +1,65 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+/**
6
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
7
+ * The only scoped value we currently infer in this program is the
8
+ * PropertyLoad `data?.toString`.
9
+ */
10
+import { useFragment } from "shared-runtime";
11
+
12
+function Foo() {
13
+ const data = useFragment();
14
+ return [data?.toString() || ""];
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Foo,
19
+ params: [],
20
+};
21
+
22
+```
23
+
24
+## Code
25
+
26
+```javascript
27
+import { unstable_useMemoCache as useMemoCache } from "react";
28
+/**
29
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
30
+ * The only scoped value we currently infer in this program is the
31
+ * PropertyLoad `data?.toString`.
32
+ */
33
+import { useFragment } from "shared-runtime";
34
+
35
+function Foo() {
36
+ const $ = useMemoCache(4);
37
+ const data = useFragment();
38
+ let t0;
39
+ if ($[0] !== data) {
40
+ t0 = data?.toString() || "";
41
+ $[0] = data;
42
+ $[1] = t0;
43
+ } else {
44
+ t0 = $[1];
45
+ }
46
+ let t1;
47
+ if ($[2] !== t0) {
48
+ t1 = [t0];
49
+ $[2] = t0;
50
+ $[3] = t1;
51
+ } else {
52
+ t1 = $[3];
53
+ }
54
+ return t1;
55
+}
56
+
57
+export const FIXTURE_ENTRYPOINT = {
58
+ fn: Foo,
59
+ params: [],
60
+};
61
+
62
+```
63
+
64
+### Eval output
65
+(kind: ok) ["[object Object]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/allocating-logical-expression-instruction-scope.ts
new
+16
@@ -0,0 +1,16 @@
1
+/**
2
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
3
+ * The only scoped value we currently infer in this program is the
4
+ * PropertyLoad `data?.toString`.
5
+ */
6
+import { useFragment } from "shared-runtime";
7
+
8
+function Foo() {
9
+ const data = useFragment();
10
+ return [data?.toString() || ""];
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: Foo,
15
+ params: [],
16
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.expect.md
new
+69
@@ -0,0 +1,69 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useRef } from "react";
6
+import { addOne } from "shared-runtime";
7
+
8
+function useKeyCommand() {
9
+ const currentPosition = useRef(0);
10
+ const handleKey = (direction) => () => {
11
+ const position = currentPosition.current;
12
+ const nextPosition = direction === "left" ? addOne(position) : position;
13
+ currentPosition.current = nextPosition;
14
+ };
15
+ const moveLeft = {
16
+ handler: handleKey("left"),
17
+ };
18
+ const moveRight = {
19
+ handler: handleKey("right"),
20
+ };
21
+ return [moveLeft, moveRight];
22
+}
23
+
24
+export const FIXTURE_ENTRYPOINT = {
25
+ fn: useKeyCommand,
26
+ params: [],
27
+};
28
+
29
+```
30
+
31
+## Code
32
+
33
+```javascript
34
+import { useRef, unstable_useMemoCache as useMemoCache } from "react";
35
+import { addOne } from "shared-runtime";
36
+
37
+function useKeyCommand() {
38
+ const $ = useMemoCache(2);
39
+ const currentPosition = useRef(0);
40
+ const handleKey = (direction) => () => {
41
+ const position = currentPosition.current;
42
+ const nextPosition = direction === "left" ? addOne(position) : position;
43
+ currentPosition.current = nextPosition;
44
+ };
45
+
46
+ const moveLeft = { handler: handleKey("left") };
47
+
48
+ const t0 = handleKey("right");
49
+ let t1;
50
+ if ($[0] !== t0) {
51
+ t1 = { handler: t0 };
52
+ $[0] = t0;
53
+ $[1] = t1;
54
+ } else {
55
+ t1 = $[1];
56
+ }
57
+ const moveRight = t1;
58
+ return [moveLeft, moveRight];
59
+}
60
+
61
+export const FIXTURE_ENTRYPOINT = {
62
+ fn: useKeyCommand,
63
+ params: [],
64
+};
65
+
66
+```
67
+
68
+### Eval output
69
+(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/capture-ref-for-later-mutation.tsx
new
+23
@@ -0,0 +1,23 @@
1
+import { useRef } from "react";
2
+import { addOne } from "shared-runtime";
3
+
4
+function useKeyCommand() {
5
+ const currentPosition = useRef(0);
6
+ const handleKey = (direction) => () => {
7
+ const position = currentPosition.current;
8
+ const nextPosition = direction === "left" ? addOne(position) : position;
9
+ currentPosition.current = nextPosition;
10
+ };
11
+ const moveLeft = {
12
+ handler: handleKey("left"),
13
+ };
14
+ const moveRight = {
15
+ handler: handleKey("right"),
16
+ };
17
+ return [moveLeft, moveRight];
18
+}
19
+
20
+export const FIXTURE_ENTRYPOINT = {
21
+ fn: useKeyCommand,
22
+ params: [],
23
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisted-declaration-with-scope.expect.md
new
+74
@@ -0,0 +1,74 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
6
+
7
+/**
8
+ * `button` and `dispatcher` must end up in the same memo block. It would be
9
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
10
+ * is created later.
11
+ */
12
+function useFoo({ onClose }) {
13
+ const button = StaticText1 ?? (
14
+ <Stringify
15
+ primary={{
16
+ label: identity("label"),
17
+ onPress: onClose,
18
+ }}
19
+ secondary={{
20
+ onPress: () => {
21
+ dispatcher.go("route2");
22
+ },
23
+ }}
24
+ />
25
+ );
26
+
27
+ const dispatcher = useHook();
28
+
29
+ return button;
30
+}
31
+
32
+export const FIXTURE_ENTRYPOINT = {
33
+ fn: useFoo,
34
+ params: [{ onClose: identity() }],
35
+};
36
+
37
+```
38
+
39
+## Code
40
+
41
+```javascript
42
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
43
+
44
+/**
45
+ * `button` and `dispatcher` must end up in the same memo block. It would be
46
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
47
+ * is created later.
48
+ */
49
+function useFoo(t0) {
50
+ const { onClose } = t0;
51
+ const button = StaticText1 ?? (
52
+ <Stringify
53
+ primary={{ label: identity("label"), onPress: onClose }}
54
+ secondary={{
55
+ onPress: () => {
56
+ dispatcher.go("route2");
57
+ },
58
+ }}
59
+ />
60
+ );
61
+
62
+ const dispatcher = useHook();
63
+ return button;
64
+}
65
+
66
+export const FIXTURE_ENTRYPOINT = {
67
+ fn: useFoo,
68
+ params: [{ onClose: identity() }],
69
+};
70
+
71
+```
72
+
73
+### Eval output
74
+(kind: ok) "[[ function params=1 ]]"
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisted-declaration-with-scope.tsx
new
+31
@@ -0,0 +1,31 @@
1
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
2
+
3
+/**
4
+ * `button` and `dispatcher` must end up in the same memo block. It would be
5
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
6
+ * is created later.
7
+ */
8
+function useFoo({ onClose }) {
9
+ const button = StaticText1 ?? (
10
+ <Stringify
11
+ primary={{
12
+ label: identity("label"),
13
+ onPress: onClose,
14
+ }}
15
+ secondary={{
16
+ onPress: () => {
17
+ dispatcher.go("route2");
18
+ },
19
+ }}
20
+ />
21
+ );
22
+
23
+ const dispatcher = useHook();
24
+
25
+ return button;
26
+}
27
+
28
+export const FIXTURE_ENTRYPOINT = {
29
+ fn: useFoo,
30
+ params: [{ onClose: identity() }],
31
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutate-outer-scope-within-value-block.expect.md
new
+94
@@ -0,0 +1,94 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
6
+
7
+/**
8
+ * There are three values with their own scopes in this fixture.
9
+ * - arr, whose mutable range extends to the `mutate(...)` call
10
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
11
+ * mutated)
12
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
13
+ * its scope range becomes extended to the value block.
14
+ *
15
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
16
+ * ```js
17
+ * [1] arr = shallowCopy() ⌝@0
18
+ * [2] cond = identity() <- @1 |
19
+ * [3] $0 = Ternary test=cond ⌝@2 |
20
+ * [4] {val : CONST_TRUE} | |
21
+ * [5] mutate(arr) | |
22
+ * [6] return $0 ⌟ ⌟
23
+ * ```
24
+ *
25
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
26
+ * should be merged.
27
+ */
28
+function useFoo({ input }) {
29
+ const arr = shallowCopy(input);
30
+
31
+ const cond = identity(false);
32
+ return cond ? { val: CONST_TRUE } : mutate(arr);
33
+}
34
+
35
+export const FIXTURE_ENTRYPOINT = {
36
+ fn: useFoo,
37
+ params: [{ input: 3 }],
38
+};
39
+
40
+```
41
+
42
+## Code
43
+
44
+```javascript
45
+import { unstable_useMemoCache as useMemoCache } from "react";
46
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
47
+
48
+/**
49
+ * There are three values with their own scopes in this fixture.
50
+ * - arr, whose mutable range extends to the `mutate(...)` call
51
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
52
+ * mutated)
53
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
54
+ * its scope range becomes extended to the value block.
55
+ *
56
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
57
+ * ```js
58
+ * [1] arr = shallowCopy() ⌝@0
59
+ * [2] cond = identity() <- @1 |
60
+ * [3] $0 = Ternary test=cond ⌝@2 |
61
+ * [4] {val : CONST_TRUE} | |
62
+ * [5] mutate(arr) | |
63
+ * [6] return $0 ⌟ ⌟
64
+ * ```
65
+ *
66
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
67
+ * should be merged.
68
+ */
69
+function useFoo(t0) {
70
+ const $ = useMemoCache(2);
71
+ const { input } = t0;
72
+ let t1;
73
+ if ($[0] !== input) {
74
+ const arr = shallowCopy(input);
75
+
76
+ const cond = identity(false);
77
+ t1 = cond ? { val: CONST_TRUE } : mutate(arr);
78
+ $[0] = input;
79
+ $[1] = t1;
80
+ } else {
81
+ t1 = $[1];
82
+ }
83
+ return t1;
84
+}
85
+
86
+export const FIXTURE_ENTRYPOINT = {
87
+ fn: useFoo,
88
+ params: [{ input: 3 }],
89
+};
90
+
91
+```
92
+
93
+### Eval output
94
+(kind: exception) mutate is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutate-outer-scope-within-value-block.ts
new
+34
@@ -0,0 +1,34 @@
1
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
2
+
3
+/**
4
+ * There are three values with their own scopes in this fixture.
5
+ * - arr, whose mutable range extends to the `mutate(...)` call
6
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
7
+ * mutated)
8
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
9
+ * its scope range becomes extended to the value block.
10
+ *
11
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
12
+ * ```js
13
+ * [1] arr = shallowCopy() ⌝@0
14
+ * [2] cond = identity() <- @1 |
15
+ * [3] $0 = Ternary test=cond ⌝@2 |
16
+ * [4] {val : CONST_TRUE} | |
17
+ * [5] mutate(arr) | |
18
+ * [6] return $0 ⌟ ⌟
19
+ * ```
20
+ *
21
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
22
+ * should be merged.
23
+ */
24
+function useFoo({ input }) {
25
+ const arr = shallowCopy(input);
26
+
27
+ const cond = identity(false);
28
+ return cond ? { val: CONST_TRUE } : mutate(arr);
29
+}
30
+
31
+export const FIXTURE_ENTRYPOINT = {
32
+ fn: useFoo,
33
+ params: [{ input: 3 }],
34
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-capture-and-mutablerange.expect.md
new
+82
@@ -0,0 +1,82 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { mutate } from "shared-runtime";
6
+
7
+/**
8
+ * This test fixture is similar to mutation-within-jsx. The only difference
9
+ * is that there is no `freeze` effect here, which means that `z` may be
10
+ * mutated after its memo block through mutating `y`.
11
+ *
12
+ * While this is technically correct (as `z` is a nested memo block), it
13
+ * is an edge case as we believe that values are not mutated after their
14
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
15
+ * values in a subsequent render.
16
+ */
17
+function useFoo({ a, b }) {
18
+ // x and y's scopes start here
19
+ const x = { a };
20
+ const y = [b];
21
+ mutate(x);
22
+ // z captures the result of `mutate(y)`, which may be aliased to `y`.
23
+ const z = [mutate(y)];
24
+ // the following line may also mutate z
25
+ mutate(y);
26
+ // and end here
27
+ return z;
28
+}
29
+
30
+export const FIXTURE_ENTRYPOINT = {
31
+ fn: useFoo,
32
+ params: [{ a: 2, b: 3 }],
33
+};
34
+
35
+```
36
+
37
+## Code
38
+
39
+```javascript
40
+import { unstable_useMemoCache as useMemoCache } from "react";
41
+import { mutate } from "shared-runtime";
42
+
43
+/**
44
+ * This test fixture is similar to mutation-within-jsx. The only difference
45
+ * is that there is no `freeze` effect here, which means that `z` may be
46
+ * mutated after its memo block through mutating `y`.
47
+ *
48
+ * While this is technically correct (as `z` is a nested memo block), it
49
+ * is an edge case as we believe that values are not mutated after their
50
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
51
+ * values in a subsequent render.
52
+ */
53
+function useFoo(t0) {
54
+ const $ = useMemoCache(3);
55
+ const { a, b } = t0;
56
+ let z;
57
+ if ($[0] !== a || $[1] !== b) {
58
+ const x = { a };
59
+ const y = [b];
60
+ mutate(x);
61
+
62
+ z = [mutate(y)];
63
+
64
+ mutate(y);
65
+ $[0] = a;
66
+ $[1] = b;
67
+ $[2] = z;
68
+ } else {
69
+ z = $[2];
70
+ }
71
+ return z;
72
+}
73
+
74
+export const FIXTURE_ENTRYPOINT = {
75
+ fn: useFoo,
76
+ params: [{ a: 2, b: 3 }],
77
+};
78
+
79
+```
80
+
81
+### Eval output
82
+(kind: ok) [null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-capture-and-mutablerange.tsx
new
+29
@@ -0,0 +1,29 @@
1
+import { mutate } from "shared-runtime";
2
+
3
+/**
4
+ * This test fixture is similar to mutation-within-jsx. The only difference
5
+ * is that there is no `freeze` effect here, which means that `z` may be
6
+ * mutated after its memo block through mutating `y`.
7
+ *
8
+ * While this is technically correct (as `z` is a nested memo block), it
9
+ * is an edge case as we believe that values are not mutated after their
10
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
11
+ * values in a subsequent render.
12
+ */
13
+function useFoo({ a, b }) {
14
+ // x and y's scopes start here
15
+ const x = { a };
16
+ const y = [b];
17
+ mutate(x);
18
+ // z captures the result of `mutate(y)`, which may be aliased to `y`.
19
+ const z = [mutate(y)];
20
+ // the following line may also mutate z
21
+ mutate(y);
22
+ // and end here
23
+ return z;
24
+}
25
+
26
+export const FIXTURE_ENTRYPOINT = {
27
+ fn: useFoo,
28
+ params: [{ a: 2, b: 3 }],
29
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-jsx-and-break.expect.md
new
+89
@@ -0,0 +1,89 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {
6
+ Stringify,
7
+ makeObject_Primitives,
8
+ mutate,
9
+ mutateAndReturn,
10
+} from "shared-runtime";
11
+
12
+function useFoo({ data }) {
13
+ let obj = null;
14
+ let myDiv = null;
15
+ label: {
16
+ if (data.cond) {
17
+ obj = makeObject_Primitives();
18
+ if (data.cond1) {
19
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
20
+ break label;
21
+ }
22
+ mutate(obj);
23
+ }
24
+ }
25
+
26
+ return myDiv;
27
+}
28
+
29
+export const FIXTURE_ENTRYPOINT = {
30
+ fn: useFoo,
31
+ params: [{ data: { cond: true, cond1: true } }],
32
+ sequentialRenders: [
33
+ { data: { cond: true, cond1: true } },
34
+ { data: { cond: true, cond1: true } },
35
+ ],
36
+};
37
+
38
+```
39
+
40
+## Code
41
+
42
+```javascript
43
+import { unstable_useMemoCache as useMemoCache } from "react";
44
+import {
45
+ Stringify,
46
+ makeObject_Primitives,
47
+ mutate,
48
+ mutateAndReturn,
49
+} from "shared-runtime";
50
+
51
+function useFoo(t0) {
52
+ const $ = useMemoCache(3);
53
+ const { data } = t0;
54
+ let obj;
55
+ let myDiv = null;
56
+ bb0: if (data.cond) {
57
+ if ($[0] !== data.cond1) {
58
+ obj = makeObject_Primitives();
59
+ if (data.cond1) {
60
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
61
+ break bb0;
62
+ }
63
+
64
+ mutate(obj);
65
+ $[0] = data.cond1;
66
+ $[1] = obj;
67
+ $[2] = myDiv;
68
+ } else {
69
+ obj = $[1];
70
+ myDiv = $[2];
71
+ }
72
+ }
73
+ return myDiv;
74
+}
75
+
76
+export const FIXTURE_ENTRYPOINT = {
77
+ fn: useFoo,
78
+ params: [{ data: { cond: true, cond1: true } }],
79
+ sequentialRenders: [
80
+ { data: { cond: true, cond1: true } },
81
+ { data: { cond: true, cond1: true } },
82
+ ],
83
+};
84
+
85
+```
86
+
87
+### Eval output
88
+(kind: ok) <div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
89
+<div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-jsx-and-break.tsx
new
+32
@@ -0,0 +1,32 @@
1
+import {
2
+ Stringify,
3
+ makeObject_Primitives,
4
+ mutate,
5
+ mutateAndReturn,
6
+} from "shared-runtime";
7
+
8
+function useFoo({ data }) {
9
+ let obj = null;
10
+ let myDiv = null;
11
+ label: {
12
+ if (data.cond) {
13
+ obj = makeObject_Primitives();
14
+ if (data.cond1) {
15
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
16
+ break label;
17
+ }
18
+ mutate(obj);
19
+ }
20
+ }
21
+
22
+ return myDiv;
23
+}
24
+
25
+export const FIXTURE_ENTRYPOINT = {
26
+ fn: useFoo,
27
+ params: [{ data: { cond: true, cond1: true } }],
28
+ sequentialRenders: [
29
+ { data: { cond: true, cond1: true } },
30
+ { data: { cond: true, cond1: true } },
31
+ ],
32
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-jsx.expect.md
new
+131
@@ -0,0 +1,131 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {
6
+ Stringify,
7
+ makeObject_Primitives,
8
+ mutateAndReturn,
9
+} from "shared-runtime";
10
+
11
+/**
12
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
13
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
14
+ * be mutated past this.
15
+ * This set of mutable range + scopes is an edge case because the JSX expression
16
+ * references values in two scopes.
17
+ * - (freeze) the result of `mutateAndReturn`
18
+ * this is a mutable value with a mutable range starting at `makeObject()`
19
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
20
+ * this is a immutable value and so gets assigned a different scope
21
+ *
22
+ * obj@0 = makeObj(); ⌝ scope@0
23
+ * if (cond) { |
24
+ * $1@0 = mutate(obj@0); |
25
+ * myDiv@1 = JSX $1@0 <- scope@1 |
26
+ * } ⌟
27
+ *
28
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
29
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
30
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
31
+ * range of `obj` (even though it is after the last mutating reference).
32
+ *
33
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
34
+ * a result, developers can never observe myDiv can aliasing a different value generation
35
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
36
+ */
37
+function useFoo({ data }) {
38
+ let obj = null;
39
+ let myDiv = null;
40
+ if (data.cond) {
41
+ obj = makeObject_Primitives();
42
+ if (data.cond1) {
43
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
44
+ }
45
+ }
46
+ return myDiv;
47
+}
48
+
49
+export const FIXTURE_ENTRYPOINT = {
50
+ fn: useFoo,
51
+ params: [{ data: { cond: true, cond1: true } }],
52
+ sequentialRenders: [
53
+ { data: { cond: true, cond1: true } },
54
+ { data: { cond: true, cond1: true } },
55
+ ],
56
+};
57
+
58
+```
59
+
60
+## Code
61
+
62
+```javascript
63
+import { unstable_useMemoCache as useMemoCache } from "react";
64
+import {
65
+ Stringify,
66
+ makeObject_Primitives,
67
+ mutateAndReturn,
68
+} from "shared-runtime";
69
+
70
+/**
71
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
72
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
73
+ * be mutated past this.
74
+ * This set of mutable range + scopes is an edge case because the JSX expression
75
+ * references values in two scopes.
76
+ * - (freeze) the result of `mutateAndReturn`
77
+ * this is a mutable value with a mutable range starting at `makeObject()`
78
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
79
+ * this is a immutable value and so gets assigned a different scope
80
+ *
81
+ * obj@0 = makeObj(); ⌝ scope@0
82
+ * if (cond) { |
83
+ * $1@0 = mutate(obj@0); |
84
+ * myDiv@1 = JSX $1@0 <- scope@1 |
85
+ * } ⌟
86
+ *
87
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
88
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
89
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
90
+ * range of `obj` (even though it is after the last mutating reference).
91
+ *
92
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
93
+ * a result, developers can never observe myDiv can aliasing a different value generation
94
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
95
+ */
96
+function useFoo(t0) {
97
+ const $ = useMemoCache(3);
98
+ const { data } = t0;
99
+ let obj;
100
+ let myDiv = null;
101
+ if (data.cond) {
102
+ if ($[0] !== data.cond1) {
103
+ obj = makeObject_Primitives();
104
+ if (data.cond1) {
105
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
106
+ }
107
+ $[0] = data.cond1;
108
+ $[1] = obj;
109
+ $[2] = myDiv;
110
+ } else {
111
+ obj = $[1];
112
+ myDiv = $[2];
113
+ }
114
+ }
115
+ return myDiv;
116
+}
117
+
118
+export const FIXTURE_ENTRYPOINT = {
119
+ fn: useFoo,
120
+ params: [{ data: { cond: true, cond1: true } }],
121
+ sequentialRenders: [
122
+ { data: { cond: true, cond1: true } },
123
+ { data: { cond: true, cond1: true } },
124
+ ],
125
+};
126
+
127
+```
128
+
129
+### Eval output
130
+(kind: ok) <div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
131
+<div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/mutation-within-jsx.tsx
new
+52
@@ -0,0 +1,52 @@
1
+import {
2
+ Stringify,
3
+ makeObject_Primitives,
4
+ mutateAndReturn,
5
+} from "shared-runtime";
6
+
7
+/**
8
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
9
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
10
+ * be mutated past this.
11
+ * This set of mutable range + scopes is an edge case because the JSX expression
12
+ * references values in two scopes.
13
+ * - (freeze) the result of `mutateAndReturn`
14
+ * this is a mutable value with a mutable range starting at `makeObject()`
15
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
16
+ * this is a immutable value and so gets assigned a different scope
17
+ *
18
+ * obj@0 = makeObj(); ⌝ scope@0
19
+ * if (cond) { |
20
+ * $1@0 = mutate(obj@0); |
21
+ * myDiv@1 = JSX $1@0 <- scope@1 |
22
+ * } ⌟
23
+ *
24
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
25
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
26
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
27
+ * range of `obj` (even though it is after the last mutating reference).
28
+ *
29
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
30
+ * a result, developers can never observe myDiv can aliasing a different value generation
31
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
32
+ */
33
+function useFoo({ data }) {
34
+ let obj = null;
35
+ let myDiv = null;
36
+ if (data.cond) {
37
+ obj = makeObject_Primitives();
38
+ if (data.cond1) {
39
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
40
+ }
41
+ }
42
+ return myDiv;
43
+}
44
+
45
+export const FIXTURE_ENTRYPOINT = {
46
+ fn: useFoo,
47
+ params: [{ data: { cond: true, cond1: true } }],
48
+ sequentialRenders: [
49
+ { data: { cond: true, cond1: true } },
50
+ { data: { cond: true, cond1: true } },
51
+ ],
52
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/nonmutating-capture-in-unsplittable-memo-block.expect.md
new
+111
@@ -0,0 +1,111 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { identity, mutate } from "shared-runtime";
6
+
7
+/**
8
+ * Currently, InferReactiveScopeVariables do not ensure that maybe-aliased
9
+ * values get assigned the same reactive scope. This is safe only when an
10
+ * already-constructed value is captured, e.g.
11
+ * ```js
12
+ * const x = makeObj(); ⌝ mutable range of x
13
+ * mutate(x); ⌟
14
+ * <-- after this point, we can produce a canonical version
15
+ * of x for all following aliases
16
+ * const y = [];
17
+ * y.push(x); <-- y captures x
18
+ * ```
19
+ *
20
+ * However, if a value is captured/aliased during its mutable range and the
21
+ * capturing container is separately memoized, it becomes difficult to guarantee
22
+ * that all aliases refer to the same value.
23
+ *
24
+ */
25
+function useFoo({ a, b }) {
26
+ const x = { a };
27
+ const y = {};
28
+ mutate(x);
29
+ const z = [identity(y), b];
30
+ mutate(y);
31
+
32
+ if (z[0] !== y) {
33
+ throw new Error("oh no!");
34
+ }
35
+ return z;
36
+}
37
+
38
+export const FIXTURE_ENTRYPOINT = {
39
+ fn: useFoo,
40
+ params: [{ a: 2, b: 3 }],
41
+ sequentialRenders: [
42
+ { a: 2, b: 3 },
43
+ { a: 4, b: 3 },
44
+ ],
45
+};
46
+
47
+```
48
+
49
+## Code
50
+
51
+```javascript
52
+import { unstable_useMemoCache as useMemoCache } from "react";
53
+import { identity, mutate } from "shared-runtime";
54
+
55
+/**
56
+ * Currently, InferReactiveScopeVariables do not ensure that maybe-aliased
57
+ * values get assigned the same reactive scope. This is safe only when an
58
+ * already-constructed value is captured, e.g.
59
+ * ```js
60
+ * const x = makeObj(); ⌝ mutable range of x
61
+ * mutate(x); ⌟
62
+ * <-- after this point, we can produce a canonical version
63
+ * of x for all following aliases
64
+ * const y = [];
65
+ * y.push(x); <-- y captures x
66
+ * ```
67
+ *
68
+ * However, if a value is captured/aliased during its mutable range and the
69
+ * capturing container is separately memoized, it becomes difficult to guarantee
70
+ * that all aliases refer to the same value.
71
+ *
72
+ */
73
+function useFoo(t0) {
74
+ const $ = useMemoCache(4);
75
+ const { a, b } = t0;
76
+ let z;
77
+ let y;
78
+ if ($[0] !== a || $[1] !== b) {
79
+ const x = { a };
80
+ y = {};
81
+ mutate(x);
82
+ z = [identity(y), b];
83
+ mutate(y);
84
+ $[0] = a;
85
+ $[1] = b;
86
+ $[2] = z;
87
+ $[3] = y;
88
+ } else {
89
+ z = $[2];
90
+ y = $[3];
91
+ }
92
+ if (z[0] !== y) {
93
+ throw new Error("oh no!");
94
+ }
95
+ return z;
96
+}
97
+
98
+export const FIXTURE_ENTRYPOINT = {
99
+ fn: useFoo,
100
+ params: [{ a: 2, b: 3 }],
101
+ sequentialRenders: [
102
+ { a: 2, b: 3 },
103
+ { a: 4, b: 3 },
104
+ ],
105
+};
106
+
107
+```
108
+
109
+### Eval output
110
+(kind: ok) [{"wat0":"joe"},3]
111
+[{"wat0":"joe"},3]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/nonmutating-capture-in-unsplittable-memo-block.ts
new
+41
@@ -0,0 +1,41 @@
1
+import { identity, mutate } from "shared-runtime";
2
+
3
+/**
4
+ * Currently, InferReactiveScopeVariables do not ensure that maybe-aliased
5
+ * values get assigned the same reactive scope. This is safe only when an
6
+ * already-constructed value is captured, e.g.
7
+ * ```js
8
+ * const x = makeObj(); ⌝ mutable range of x
9
+ * mutate(x); ⌟
10
+ * <-- after this point, we can produce a canonical version
11
+ * of x for all following aliases
12
+ * const y = [];
13
+ * y.push(x); <-- y captures x
14
+ * ```
15
+ *
16
+ * However, if a value is captured/aliased during its mutable range and the
17
+ * capturing container is separately memoized, it becomes difficult to guarantee
18
+ * that all aliases refer to the same value.
19
+ *
20
+ */
21
+function useFoo({ a, b }) {
22
+ const x = { a };
23
+ const y = {};
24
+ mutate(x);
25
+ const z = [identity(y), b];
26
+ mutate(y);
27
+
28
+ if (z[0] !== y) {
29
+ throw new Error("oh no!");
30
+ }
31
+ return z;
32
+}
33
+
34
+export const FIXTURE_ENTRYPOINT = {
35
+ fn: useFoo,
36
+ params: [{ a: 2, b: 3 }],
37
+ sequentialRenders: [
38
+ { a: 2, b: 3 },
39
+ { a: 4, b: 3 },
40
+ ],
41
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/allocating-logical-expression-instruction-scope.expect.md
new
+61
@@ -0,0 +1,61 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+
7
+/**
8
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
9
+ * The only scoped value we currently infer in this program is the
10
+ * PropertyLoad `data?.toString`.
11
+ */
12
+import { useFragment } from "shared-runtime";
13
+
14
+function Foo() {
15
+ const data = useFragment();
16
+ return [data?.toString() || ""];
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: Foo,
21
+ params: [],
22
+};
23
+
24
+```
25
+
26
+## Code
27
+
28
+```javascript
29
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
30
+
31
+/**
32
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
33
+ * The only scoped value we currently infer in this program is the
34
+ * PropertyLoad `data?.toString`.
35
+ */
36
+import { useFragment } from "shared-runtime";
37
+
38
+function Foo() {
39
+ const $ = useMemoCache(2);
40
+ const data = useFragment();
41
+ const t0 = data?.toString() || "";
42
+ let t1;
43
+ if ($[0] !== t0) {
44
+ t1 = [t0];
45
+ $[0] = t0;
46
+ $[1] = t1;
47
+ } else {
48
+ t1 = $[1];
49
+ }
50
+ return t1;
51
+}
52
+
53
+export const FIXTURE_ENTRYPOINT = {
54
+ fn: Foo,
55
+ params: [],
56
+};
57
+
58
+```
59
+
60
+### Eval output
61
+(kind: ok) ["[object Object]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/allocating-logical-expression-instruction-scope.ts
new
+18
@@ -0,0 +1,18 @@
1
+// @enableReactiveScopesInHIR:false
2
+
3
+/**
4
+ * This is a weird case as data has type `BuiltInMixedReadonly`.
5
+ * The only scoped value we currently infer in this program is the
6
+ * PropertyLoad `data?.toString`.
7
+ */
8
+import { useFragment } from "shared-runtime";
9
+
10
+function Foo() {
11
+ const data = useFragment();
12
+ return [data?.toString() || ""];
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Foo,
17
+ params: [],
18
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/bug-hoisted-declaration-with-scope.expect.md
new
+96
@@ -0,0 +1,96 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
7
+/**
8
+ * `button` and `dispatcher` must end up in the same memo block. It would be
9
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
10
+ * is created later.
11
+ *
12
+ * Sprout error:
13
+ * Found differences in evaluator results
14
+ * Non-forget (expected):
15
+ * (kind: ok) "[[ function params=1 ]]"
16
+ * Forget:
17
+ * (kind: exception) Cannot access 'dispatcher' before initialization
18
+ */
19
+function useFoo({ onClose }) {
20
+ const button = StaticText1 ?? (
21
+ <Stringify
22
+ primary={{
23
+ label: identity("label"),
24
+ onPress: onClose,
25
+ }}
26
+ secondary={{
27
+ onPress: () => {
28
+ dispatcher.go("route2");
29
+ },
30
+ }}
31
+ />
32
+ );
33
+
34
+ const dispatcher = useHook();
35
+
36
+ return button;
37
+}
38
+
39
+export const FIXTURE_ENTRYPOINT = {
40
+ fn: useFoo,
41
+ params: [{ onClose: identity() }],
42
+};
43
+
44
+```
45
+
46
+## Code
47
+
48
+```javascript
49
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
50
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
51
+/**
52
+ * `button` and `dispatcher` must end up in the same memo block. It would be
53
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
54
+ * is created later.
55
+ *
56
+ * Sprout error:
57
+ * Found differences in evaluator results
58
+ * Non-forget (expected):
59
+ * (kind: ok) "[[ function params=1 ]]"
60
+ * Forget:
61
+ * (kind: exception) Cannot access 'dispatcher' before initialization
62
+ */
63
+function useFoo(t0) {
64
+ const $ = useMemoCache(3);
65
+ const { onClose } = t0;
66
+ let t1;
67
+ if ($[0] !== onClose || $[1] !== dispatcher) {
68
+ t1 = StaticText1 ?? (
69
+ <Stringify
70
+ primary={{ label: identity("label"), onPress: onClose }}
71
+ secondary={{
72
+ onPress: () => {
73
+ dispatcher.go("route2");
74
+ },
75
+ }}
76
+ />
77
+ );
78
+ $[0] = onClose;
79
+ $[1] = dispatcher;
80
+ $[2] = t1;
81
+ } else {
82
+ t1 = $[2];
83
+ }
84
+ const button = t1;
85
+
86
+ const dispatcher = useHook();
87
+ return button;
88
+}
89
+
90
+export const FIXTURE_ENTRYPOINT = {
91
+ fn: useFoo,
92
+ params: [{ onClose: identity() }],
93
+};
94
+
95
+```
96
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/bug-hoisted-declaration-with-scope.tsx
new
+38
@@ -0,0 +1,38 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { StaticText1, Stringify, identity, useHook } from "shared-runtime";
3
+/**
4
+ * `button` and `dispatcher` must end up in the same memo block. It would be
5
+ * invalid for `button` to take a dependency on `dispatcher` as dispatcher
6
+ * is created later.
7
+ *
8
+ * Sprout error:
9
+ * Found differences in evaluator results
10
+ * Non-forget (expected):
11
+ * (kind: ok) "[[ function params=1 ]]"
12
+ * Forget:
13
+ * (kind: exception) Cannot access 'dispatcher' before initialization
14
+ */
15
+function useFoo({ onClose }) {
16
+ const button = StaticText1 ?? (
17
+ <Stringify
18
+ primary={{
19
+ label: identity("label"),
20
+ onPress: onClose,
21
+ }}
22
+ secondary={{
23
+ onPress: () => {
24
+ dispatcher.go("route2");
25
+ },
26
+ }}
27
+ />
28
+ );
29
+
30
+ const dispatcher = useHook();
31
+
32
+ return button;
33
+}
34
+
35
+export const FIXTURE_ENTRYPOINT = {
36
+ fn: useFoo,
37
+ params: [{ onClose: identity() }],
38
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block.expect.md
new
+137
@@ -0,0 +1,137 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { identity, mutate } from "shared-runtime";
7
+
8
+/**
9
+ * The root cause of this bug is in `InferReactiveScopeVariables`. Currently,
10
+ * InferReactiveScopeVariables do not ensure that maybe-aliased values get
11
+ * assigned the same reactive scope. This is safe only when an already-
12
+ * constructed value is captured, e.g.
13
+ * ```js
14
+ * const x = makeObj(); ⌝ mutable range of x
15
+ * mutate(x); ⌟
16
+ * <-- after this point, we can produce a canonical version
17
+ * of x for all following aliases
18
+ * const y = [];
19
+ * y.push(x); <-- y captures x
20
+ * ```
21
+ *
22
+ * However, if a value is captured/aliased during its mutable range and the
23
+ * capturing container is separately memoized, it becomes difficult to guarantee
24
+ * that all aliases refer to the same value.
25
+ *
26
+ * Sprout error:
27
+ * Found differences in evaluator results
28
+ * Non-forget (expected):
29
+ * (kind: ok) [{"wat0":"joe"},3]
30
+ * [{"wat0":"joe"},3]
31
+ * Forget:
32
+ * (kind: ok) [{"wat0":"joe"},3]
33
+ * [[ (exception in render) Error: oh no! ]]
34
+ *
35
+ */
36
+function useFoo({ a, b }) {
37
+ const x = { a };
38
+ const y = {};
39
+ mutate(x);
40
+ const z = [identity(y), b];
41
+ mutate(y);
42
+
43
+ if (z[0] !== y) {
44
+ throw new Error("oh no!");
45
+ }
46
+ return z;
47
+}
48
+
49
+export const FIXTURE_ENTRYPOINT = {
50
+ fn: useFoo,
51
+ params: [{ a: 2, b: 3 }],
52
+ sequentialRenders: [
53
+ { a: 2, b: 3 },
54
+ { a: 4, b: 3 },
55
+ ],
56
+};
57
+
58
+```
59
+
60
+## Code
61
+
62
+```javascript
63
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
64
+import { identity, mutate } from "shared-runtime";
65
+
66
+/**
67
+ * The root cause of this bug is in `InferReactiveScopeVariables`. Currently,
68
+ * InferReactiveScopeVariables do not ensure that maybe-aliased values get
69
+ * assigned the same reactive scope. This is safe only when an already-
70
+ * constructed value is captured, e.g.
71
+ * ```js
72
+ * const x = makeObj(); ⌝ mutable range of x
73
+ * mutate(x); ⌟
74
+ * <-- after this point, we can produce a canonical version
75
+ * of x for all following aliases
76
+ * const y = [];
77
+ * y.push(x); <-- y captures x
78
+ * ```
79
+ *
80
+ * However, if a value is captured/aliased during its mutable range and the
81
+ * capturing container is separately memoized, it becomes difficult to guarantee
82
+ * that all aliases refer to the same value.
83
+ *
84
+ * Sprout error:
85
+ * Found differences in evaluator results
86
+ * Non-forget (expected):
87
+ * (kind: ok) [{"wat0":"joe"},3]
88
+ * [{"wat0":"joe"},3]
89
+ * Forget:
90
+ * (kind: ok) [{"wat0":"joe"},3]
91
+ * [[ (exception in render) Error: oh no! ]]
92
+ *
93
+ */
94
+function useFoo(t0) {
95
+ const $ = useMemoCache(6);
96
+ const { a, b } = t0;
97
+ let z;
98
+ let y;
99
+ if ($[0] !== a || $[1] !== b) {
100
+ const x = { a };
101
+ y = {};
102
+ mutate(x);
103
+ let t1;
104
+ if ($[4] !== b) {
105
+ t1 = [identity(y), b];
106
+ $[4] = b;
107
+ $[5] = t1;
108
+ } else {
109
+ t1 = $[5];
110
+ }
111
+ z = t1;
112
+ mutate(y);
113
+ $[0] = a;
114
+ $[1] = b;
115
+ $[2] = z;
116
+ $[3] = y;
117
+ } else {
118
+ z = $[2];
119
+ y = $[3];
120
+ }
121
+ if (z[0] !== y) {
122
+ throw new Error("oh no!");
123
+ }
124
+ return z;
125
+}
126
+
127
+export const FIXTURE_ENTRYPOINT = {
128
+ fn: useFoo,
129
+ params: [{ a: 2, b: 3 }],
130
+ sequentialRenders: [
131
+ { a: 2, b: 3 },
132
+ { a: 4, b: 3 },
133
+ ],
134
+};
135
+
136
+```
137
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block.ts
new
+52
@@ -0,0 +1,52 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { identity, mutate } from "shared-runtime";
3
+
4
+/**
5
+ * The root cause of this bug is in `InferReactiveScopeVariables`. Currently,
6
+ * InferReactiveScopeVariables do not ensure that maybe-aliased values get
7
+ * assigned the same reactive scope. This is safe only when an already-
8
+ * constructed value is captured, e.g.
9
+ * ```js
10
+ * const x = makeObj(); ⌝ mutable range of x
11
+ * mutate(x); ⌟
12
+ * <-- after this point, we can produce a canonical version
13
+ * of x for all following aliases
14
+ * const y = [];
15
+ * y.push(x); <-- y captures x
16
+ * ```
17
+ *
18
+ * However, if a value is captured/aliased during its mutable range and the
19
+ * capturing container is separately memoized, it becomes difficult to guarantee
20
+ * that all aliases refer to the same value.
21
+ *
22
+ * Sprout error:
23
+ * Found differences in evaluator results
24
+ * Non-forget (expected):
25
+ * (kind: ok) [{"wat0":"joe"},3]
26
+ * [{"wat0":"joe"},3]
27
+ * Forget:
28
+ * (kind: ok) [{"wat0":"joe"},3]
29
+ * [[ (exception in render) Error: oh no! ]]
30
+ *
31
+ */
32
+function useFoo({ a, b }) {
33
+ const x = { a };
34
+ const y = {};
35
+ mutate(x);
36
+ const z = [identity(y), b];
37
+ mutate(y);
38
+
39
+ if (z[0] !== y) {
40
+ throw new Error("oh no!");
41
+ }
42
+ return z;
43
+}
44
+
45
+export const FIXTURE_ENTRYPOINT = {
46
+ fn: useFoo,
47
+ params: [{ a: 2, b: 3 }],
48
+ sequentialRenders: [
49
+ { a: 2, b: 3 },
50
+ { a: 4, b: 3 },
51
+ ],
52
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.expect.md
new
+89
@@ -0,0 +1,89 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { useRef } from "react";
7
+import { addOne } from "shared-runtime";
8
+
9
+function useKeyCommand() {
10
+ const currentPosition = useRef(0);
11
+ const handleKey = (direction) => () => {
12
+ const position = currentPosition.current;
13
+ const nextPosition = direction === "left" ? addOne(position) : position;
14
+ currentPosition.current = nextPosition;
15
+ };
16
+ const moveLeft = {
17
+ handler: handleKey("left"),
18
+ };
19
+ const moveRight = {
20
+ handler: handleKey("right"),
21
+ };
22
+ return [moveLeft, moveRight];
23
+}
24
+
25
+export const FIXTURE_ENTRYPOINT = {
26
+ fn: useKeyCommand,
27
+ params: [],
28
+};
29
+
30
+```
31
+
32
+## Code
33
+
34
+```javascript
35
+// @enableReactiveScopesInHIR:false
36
+import { useRef, unstable_useMemoCache as useMemoCache } from "react";
37
+import { addOne } from "shared-runtime";
38
+
39
+function useKeyCommand() {
40
+ const $ = useMemoCache(7);
41
+ const currentPosition = useRef(0);
42
+ const handleKey = (direction) => () => {
43
+ const position = currentPosition.current;
44
+ const nextPosition = direction === "left" ? addOne(position) : position;
45
+ currentPosition.current = nextPosition;
46
+ };
47
+
48
+ const t0 = handleKey("left");
49
+ let t1;
50
+ if ($[0] !== t0) {
51
+ t1 = { handler: t0 };
52
+ $[0] = t0;
53
+ $[1] = t1;
54
+ } else {
55
+ t1 = $[1];
56
+ }
57
+ const moveLeft = t1;
58
+
59
+ const t2 = handleKey("right");
60
+ let t3;
61
+ if ($[2] !== t2) {
62
+ t3 = { handler: t2 };
63
+ $[2] = t2;
64
+ $[3] = t3;
65
+ } else {
66
+ t3 = $[3];
67
+ }
68
+ const moveRight = t3;
69
+ let t4;
70
+ if ($[4] !== moveLeft || $[5] !== moveRight) {
71
+ t4 = [moveLeft, moveRight];
72
+ $[4] = moveLeft;
73
+ $[5] = moveRight;
74
+ $[6] = t4;
75
+ } else {
76
+ t4 = $[6];
77
+ }
78
+ return t4;
79
+}
80
+
81
+export const FIXTURE_ENTRYPOINT = {
82
+ fn: useKeyCommand,
83
+ params: [],
84
+};
85
+
86
+```
87
+
88
+### Eval output
89
+(kind: ok) [{"handler":"[[ function params=0 ]]"},{"handler":"[[ function params=0 ]]"}]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/capture-ref-for-later-mutation.tsx
new
+24
@@ -0,0 +1,24 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { useRef } from "react";
3
+import { addOne } from "shared-runtime";
4
+
5
+function useKeyCommand() {
6
+ const currentPosition = useRef(0);
7
+ const handleKey = (direction) => () => {
8
+ const position = currentPosition.current;
9
+ const nextPosition = direction === "left" ? addOne(position) : position;
10
+ currentPosition.current = nextPosition;
11
+ };
12
+ const moveLeft = {
13
+ handler: handleKey("left"),
14
+ };
15
+ const moveRight = {
16
+ handler: handleKey("right"),
17
+ };
18
+ return [moveLeft, moveRight];
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: useKeyCommand,
23
+ params: [],
24
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/invalid-align-scopes-within-nested-valueblock-in-array.expect.md
new
+84
@@ -0,0 +1,84 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+
7
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
8
+
9
+/**
10
+ * Here, identity('foo') is an immutable allocating instruction.
11
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
12
+ *
13
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
14
+ * both scopes to end at value blocks within the <></> expression.
15
+ * However, both scope ranges should be aligned to the outer value block
16
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
17
+ * handles this correctly.
18
+ */
19
+function Foo({ cond1, cond2 }) {
20
+ const arr = makeArray<any>({ a: 2 }, 2, []);
21
+
22
+ return cond1 ? (
23
+ <>
24
+ <div>{identity("foo")}</div>
25
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
26
+ </>
27
+ ) : null;
28
+}
29
+
30
+export const FIXTURE_ENTRYPOINT = {
31
+ fn: Foo,
32
+ params: [{ cond1: true, cond2: true }],
33
+};
34
+
35
+```
36
+
37
+## Code
38
+
39
+```javascript
40
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
41
+
42
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
43
+
44
+/**
45
+ * Here, identity('foo') is an immutable allocating instruction.
46
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
47
+ *
48
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
49
+ * both scopes to end at value blocks within the <></> expression.
50
+ * However, both scope ranges should be aligned to the outer value block
51
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
52
+ * handles this correctly.
53
+ */
54
+function Foo(t0) {
55
+ const $ = useMemoCache(4);
56
+ const { cond1, cond2 } = t0;
57
+ const arr = makeArray({ a: 2 }, 2, []);
58
+ let t1;
59
+ if ($[0] !== cond1 || $[1] !== cond2 || $[2] !== arr) {
60
+ t1 = cond1 ? (
61
+ <>
62
+ <div>{identity("foo")}</div>
63
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
64
+ </>
65
+ ) : null;
66
+ $[0] = cond1;
67
+ $[1] = cond2;
68
+ $[2] = arr;
69
+ $[3] = t1;
70
+ } else {
71
+ t1 = $[3];
72
+ }
73
+ return t1;
74
+}
75
+
76
+export const FIXTURE_ENTRYPOINT = {
77
+ fn: Foo,
78
+ params: [{ cond1: true, cond2: true }],
79
+};
80
+
81
+```
82
+
83
+### Eval output
84
+(kind: ok) <div>foo</div><div>{"value":[null,null,null]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/invalid-align-scopes-within-nested-valueblock-in-array.tsx
new
+29
@@ -0,0 +1,29 @@
1
+// @enableReactiveScopesInHIR:false
2
+
3
+import { Stringify, identity, makeArray, mutate } from "shared-runtime";
4
+
5
+/**
6
+ * Here, identity('foo') is an immutable allocating instruction.
7
+ * `arr` is a mutable value whose mutable range ends at `arr.map`.
8
+ *
9
+ * The previous (reactive function) version of alignScopesToBlocks set the range of
10
+ * both scopes to end at value blocks within the <></> expression.
11
+ * However, both scope ranges should be aligned to the outer value block
12
+ * (e.g. `cond1 ? <>: null`). The HIR version of alignScopesToBlocks
13
+ * handles this correctly.
14
+ */
15
+function Foo({ cond1, cond2 }) {
16
+ const arr = makeArray<any>({ a: 2 }, 2, []);
17
+
18
+ return cond1 ? (
19
+ <>
20
+ <div>{identity("foo")}</div>
21
+ <Stringify value={cond2 ? arr.map(mutate) : null} />
22
+ </>
23
+ ) : null;
24
+}
25
+
26
+export const FIXTURE_ENTRYPOINT = {
27
+ fn: Foo,
28
+ params: [{ cond1: true, cond2: true }],
29
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutate-outer-scope-within-value-block.expect.md
new
+101
@@ -0,0 +1,101 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
7
+
8
+/**
9
+ * There are three values with their own scopes in this fixture.
10
+ * - arr, whose mutable range extends to the `mutate(...)` call
11
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
12
+ * mutated)
13
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
14
+ * its scope range becomes extended to the value block.
15
+ *
16
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
17
+ * ```js
18
+ * [1] arr = shallowCopy() ⌝@0
19
+ * [2] cond = identity() <- @1 |
20
+ * [3] $0 = Ternary test=cond ⌝@2 |
21
+ * [4] {val : CONST_TRUE} | |
22
+ * [5] mutate(arr) | |
23
+ * [6] return $0 ⌟ ⌟
24
+ * ```
25
+ *
26
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
27
+ * should be merged.
28
+ */
29
+function useFoo({ input }) {
30
+ const arr = shallowCopy(input);
31
+
32
+ const cond = identity(false);
33
+ return cond ? { val: CONST_TRUE } : mutate(arr);
34
+}
35
+
36
+export const FIXTURE_ENTRYPOINT = {
37
+ fn: useFoo,
38
+ params: [{ input: 3 }],
39
+};
40
+
41
+```
42
+
43
+## Code
44
+
45
+```javascript
46
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
47
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
48
+
49
+/**
50
+ * There are three values with their own scopes in this fixture.
51
+ * - arr, whose mutable range extends to the `mutate(...)` call
52
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
53
+ * mutated)
54
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
55
+ * its scope range becomes extended to the value block.
56
+ *
57
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
58
+ * ```js
59
+ * [1] arr = shallowCopy() ⌝@0
60
+ * [2] cond = identity() <- @1 |
61
+ * [3] $0 = Ternary test=cond ⌝@2 |
62
+ * [4] {val : CONST_TRUE} | |
63
+ * [5] mutate(arr) | |
64
+ * [6] return $0 ⌟ ⌟
65
+ * ```
66
+ *
67
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
68
+ * should be merged.
69
+ */
70
+function useFoo(t0) {
71
+ const $ = useMemoCache(3);
72
+ const { input } = t0;
73
+ const arr = shallowCopy(input);
74
+ let t1;
75
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
76
+ t1 = identity(false);
77
+ $[0] = t1;
78
+ } else {
79
+ t1 = $[0];
80
+ }
81
+ const cond = t1;
82
+ let t2;
83
+ if ($[1] !== arr) {
84
+ t2 = cond ? { val: CONST_TRUE } : mutate(arr);
85
+ $[1] = arr;
86
+ $[2] = t2;
87
+ } else {
88
+ t2 = $[2];
89
+ }
90
+ return t2;
91
+}
92
+
93
+export const FIXTURE_ENTRYPOINT = {
94
+ fn: useFoo,
95
+ params: [{ input: 3 }],
96
+};
97
+
98
+```
99
+
100
+### Eval output
101
+(kind: exception) mutate is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutate-outer-scope-within-value-block.ts
new
+35
@@ -0,0 +1,35 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { CONST_TRUE, identity, shallowCopy } from "shared-runtime";
3
+
4
+/**
5
+ * There are three values with their own scopes in this fixture.
6
+ * - arr, whose mutable range extends to the `mutate(...)` call
7
+ * - cond, which has a mutable range of exactly 1 (e.g. created but not
8
+ * mutated)
9
+ * - { val: CONST_TRUE }, which is also not mutated after creation. However,
10
+ * its scope range becomes extended to the value block.
11
+ *
12
+ * After AlignScopesToBlockScopes, our scopes look roughly like this
13
+ * ```js
14
+ * [1] arr = shallowCopy() ⌝@0
15
+ * [2] cond = identity() <- @1 |
16
+ * [3] $0 = Ternary test=cond ⌝@2 |
17
+ * [4] {val : CONST_TRUE} | |
18
+ * [5] mutate(arr) | |
19
+ * [6] return $0 ⌟ ⌟
20
+ * ```
21
+ *
22
+ * Observe that instruction 5 mutates scope 0, which means that scopes 0 and 2
23
+ * should be merged.
24
+ */
25
+function useFoo({ input }) {
26
+ const arr = shallowCopy(input);
27
+
28
+ const cond = identity(false);
29
+ return cond ? { val: CONST_TRUE } : mutate(arr);
30
+}
31
+
32
+export const FIXTURE_ENTRYPOINT = {
33
+ fn: useFoo,
34
+ params: [{ input: 3 }],
35
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-capture-and-mutablerange.expect.md
new
+92
@@ -0,0 +1,92 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { mutate } from "shared-runtime";
7
+
8
+/**
9
+ * This test fixture is similar to mutation-within-jsx. The only difference
10
+ * is that there is no `freeze` effect here, which means that `z` may be
11
+ * mutated after its memo block through mutating `y`.
12
+ *
13
+ * While this is technically correct (as `z` is a nested memo block), it
14
+ * is an edge case as we believe that values are not mutated after their
15
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
16
+ * values in a subsequent render.
17
+ */
18
+function useFoo({ a, b }) {
19
+ // x and y's scopes start here
20
+ const x = { a };
21
+ const y = [b];
22
+ mutate(x);
23
+ // z captures the result of `mutate(y)`, which may be aliased to `y`.
24
+ const z = [mutate(y)];
25
+ // the following line may also mutate z
26
+ mutate(y);
27
+ // and end here
28
+ return z;
29
+}
30
+
31
+export const FIXTURE_ENTRYPOINT = {
32
+ fn: useFoo,
33
+ params: [{ a: 2, b: 3 }],
34
+};
35
+
36
+```
37
+
38
+## Code
39
+
40
+```javascript
41
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
42
+import { mutate } from "shared-runtime";
43
+
44
+/**
45
+ * This test fixture is similar to mutation-within-jsx. The only difference
46
+ * is that there is no `freeze` effect here, which means that `z` may be
47
+ * mutated after its memo block through mutating `y`.
48
+ *
49
+ * While this is technically correct (as `z` is a nested memo block), it
50
+ * is an edge case as we believe that values are not mutated after their
51
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
52
+ * values in a subsequent render.
53
+ */
54
+function useFoo(t0) {
55
+ const $ = useMemoCache(5);
56
+ const { a, b } = t0;
57
+ let z;
58
+ if ($[0] !== a || $[1] !== b) {
59
+ const x = { a };
60
+ const y = [b];
61
+ mutate(x);
62
+
63
+ const t1 = mutate(y);
64
+ let t2;
65
+ if ($[3] !== t1) {
66
+ t2 = [t1];
67
+ $[3] = t1;
68
+ $[4] = t2;
69
+ } else {
70
+ t2 = $[4];
71
+ }
72
+ z = t2;
73
+
74
+ mutate(y);
75
+ $[0] = a;
76
+ $[1] = b;
77
+ $[2] = z;
78
+ } else {
79
+ z = $[2];
80
+ }
81
+ return z;
82
+}
83
+
84
+export const FIXTURE_ENTRYPOINT = {
85
+ fn: useFoo,
86
+ params: [{ a: 2, b: 3 }],
87
+};
88
+
89
+```
90
+
91
+### Eval output
92
+(kind: ok) [null]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-capture-and-mutablerange.tsx
new
+30
@@ -0,0 +1,30 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { mutate } from "shared-runtime";
3
+
4
+/**
5
+ * This test fixture is similar to mutation-within-jsx. The only difference
6
+ * is that there is no `freeze` effect here, which means that `z` may be
7
+ * mutated after its memo block through mutating `y`.
8
+ *
9
+ * While this is technically correct (as `z` is a nested memo block), it
10
+ * is an edge case as we believe that values are not mutated after their
11
+ * memo blocks (which may lead to 'tearing', i.e. mutating one render's
12
+ * values in a subsequent render.
13
+ */
14
+function useFoo({ a, b }) {
15
+ // x and y's scopes start here
16
+ const x = { a };
17
+ const y = [b];
18
+ mutate(x);
19
+ // z captures the result of `mutate(y)`, which may be aliased to `y`.
20
+ const z = [mutate(y)];
21
+ // the following line may also mutate z
22
+ mutate(y);
23
+ // and end here
24
+ return z;
25
+}
26
+
27
+export const FIXTURE_ENTRYPOINT = {
28
+ fn: useFoo,
29
+ params: [{ a: 2, b: 3 }],
30
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-jsx-and-break.expect.md
new
+99
@@ -0,0 +1,99 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import {
7
+ Stringify,
8
+ makeObject_Primitives,
9
+ mutate,
10
+ mutateAndReturn,
11
+} from "shared-runtime";
12
+
13
+function useFoo({ data }) {
14
+ let obj = null;
15
+ let myDiv = null;
16
+ label: {
17
+ if (data.cond) {
18
+ obj = makeObject_Primitives();
19
+ if (data.cond1) {
20
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
21
+ break label;
22
+ }
23
+ mutate(obj);
24
+ }
25
+ }
26
+
27
+ return myDiv;
28
+}
29
+
30
+export const FIXTURE_ENTRYPOINT = {
31
+ fn: useFoo,
32
+ params: [{ data: { cond: true, cond1: true } }],
33
+ sequentialRenders: [
34
+ { data: { cond: true, cond1: true } },
35
+ { data: { cond: true, cond1: true } },
36
+ ],
37
+};
38
+
39
+```
40
+
41
+## Code
42
+
43
+```javascript
44
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
45
+import {
46
+ Stringify,
47
+ makeObject_Primitives,
48
+ mutate,
49
+ mutateAndReturn,
50
+} from "shared-runtime";
51
+
52
+function useFoo(t0) {
53
+ const $ = useMemoCache(5);
54
+ const { data } = t0;
55
+ let obj;
56
+ let myDiv = null;
57
+ bb0: if (data.cond) {
58
+ if ($[0] !== data.cond1) {
59
+ obj = makeObject_Primitives();
60
+ if (data.cond1) {
61
+ const t1 = mutateAndReturn(obj);
62
+ let t2;
63
+ if ($[3] !== t1) {
64
+ t2 = <Stringify value={t1} />;
65
+ $[3] = t1;
66
+ $[4] = t2;
67
+ } else {
68
+ t2 = $[4];
69
+ }
70
+ myDiv = t2;
71
+ break bb0;
72
+ }
73
+
74
+ mutate(obj);
75
+ $[0] = data.cond1;
76
+ $[1] = obj;
77
+ $[2] = myDiv;
78
+ } else {
79
+ obj = $[1];
80
+ myDiv = $[2];
81
+ }
82
+ }
83
+ return myDiv;
84
+}
85
+
86
+export const FIXTURE_ENTRYPOINT = {
87
+ fn: useFoo,
88
+ params: [{ data: { cond: true, cond1: true } }],
89
+ sequentialRenders: [
90
+ { data: { cond: true, cond1: true } },
91
+ { data: { cond: true, cond1: true } },
92
+ ],
93
+};
94
+
95
+```
96
+
97
+### Eval output
98
+(kind: ok) <div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
99
+<div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-jsx-and-break.tsx
new
+33
@@ -0,0 +1,33 @@
1
+// @enableReactiveScopesInHIR:false
2
+import {
3
+ Stringify,
4
+ makeObject_Primitives,
5
+ mutate,
6
+ mutateAndReturn,
7
+} from "shared-runtime";
8
+
9
+function useFoo({ data }) {
10
+ let obj = null;
11
+ let myDiv = null;
12
+ label: {
13
+ if (data.cond) {
14
+ obj = makeObject_Primitives();
15
+ if (data.cond1) {
16
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
17
+ break label;
18
+ }
19
+ mutate(obj);
20
+ }
21
+ }
22
+
23
+ return myDiv;
24
+}
25
+
26
+export const FIXTURE_ENTRYPOINT = {
27
+ fn: useFoo,
28
+ params: [{ data: { cond: true, cond1: true } }],
29
+ sequentialRenders: [
30
+ { data: { cond: true, cond1: true } },
31
+ { data: { cond: true, cond1: true } },
32
+ ],
33
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-jsx.expect.md
new
+141
@@ -0,0 +1,141 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import {
7
+ Stringify,
8
+ makeObject_Primitives,
9
+ mutateAndReturn,
10
+} from "shared-runtime";
11
+
12
+/**
13
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
14
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
15
+ * be mutated past this.
16
+ * This set of mutable range + scopes is an edge case because the JSX expression
17
+ * references values in two scopes.
18
+ * - (freeze) the result of `mutateAndReturn`
19
+ * this is a mutable value with a mutable range starting at `makeObject()`
20
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
21
+ * this is a immutable value and so gets assigned a different scope
22
+ *
23
+ * obj@0 = makeObj(); ⌝ scope@0
24
+ * if (cond) { |
25
+ * $1@0 = mutate(obj@0); |
26
+ * myDiv@1 = JSX $1@0 <- scope@1 |
27
+ * } ⌟
28
+ *
29
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
30
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
31
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
32
+ * range of `obj` (even though it is after the last mutating reference).
33
+ *
34
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
35
+ * a result, developers can never observe myDiv can aliasing a different value generation
36
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
37
+ */
38
+function useFoo({ data }) {
39
+ let obj = null;
40
+ let myDiv = null;
41
+ if (data.cond) {
42
+ obj = makeObject_Primitives();
43
+ if (data.cond1) {
44
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
45
+ }
46
+ }
47
+ return myDiv;
48
+}
49
+
50
+export const FIXTURE_ENTRYPOINT = {
51
+ fn: useFoo,
52
+ params: [{ data: { cond: true, cond1: true } }],
53
+ sequentialRenders: [
54
+ { data: { cond: true, cond1: true } },
55
+ { data: { cond: true, cond1: true } },
56
+ ],
57
+};
58
+
59
+```
60
+
61
+## Code
62
+
63
+```javascript
64
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
65
+import {
66
+ Stringify,
67
+ makeObject_Primitives,
68
+ mutateAndReturn,
69
+} from "shared-runtime";
70
+
71
+/**
72
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
73
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
74
+ * be mutated past this.
75
+ * This set of mutable range + scopes is an edge case because the JSX expression
76
+ * references values in two scopes.
77
+ * - (freeze) the result of `mutateAndReturn`
78
+ * this is a mutable value with a mutable range starting at `makeObject()`
79
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
80
+ * this is a immutable value and so gets assigned a different scope
81
+ *
82
+ * obj@0 = makeObj(); ⌝ scope@0
83
+ * if (cond) { |
84
+ * $1@0 = mutate(obj@0); |
85
+ * myDiv@1 = JSX $1@0 <- scope@1 |
86
+ * } ⌟
87
+ *
88
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
89
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
90
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
91
+ * range of `obj` (even though it is after the last mutating reference).
92
+ *
93
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
94
+ * a result, developers can never observe myDiv can aliasing a different value generation
95
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
96
+ */
97
+function useFoo(t0) {
98
+ const $ = useMemoCache(5);
99
+ const { data } = t0;
100
+ let obj;
101
+ let myDiv = null;
102
+ if (data.cond) {
103
+ if ($[0] !== data.cond1) {
104
+ obj = makeObject_Primitives();
105
+ if (data.cond1) {
106
+ const t1 = mutateAndReturn(obj);
107
+ let t2;
108
+ if ($[3] !== t1) {
109
+ t2 = <Stringify value={t1} />;
110
+ $[3] = t1;
111
+ $[4] = t2;
112
+ } else {
113
+ t2 = $[4];
114
+ }
115
+ myDiv = t2;
116
+ }
117
+ $[0] = data.cond1;
118
+ $[1] = obj;
119
+ $[2] = myDiv;
120
+ } else {
121
+ obj = $[1];
122
+ myDiv = $[2];
123
+ }
124
+ }
125
+ return myDiv;
126
+}
127
+
128
+export const FIXTURE_ENTRYPOINT = {
129
+ fn: useFoo,
130
+ params: [{ data: { cond: true, cond1: true } }],
131
+ sequentialRenders: [
132
+ { data: { cond: true, cond1: true } },
133
+ { data: { cond: true, cond1: true } },
134
+ ],
135
+};
136
+
137
+```
138
+
139
+### Eval output
140
+(kind: ok) <div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
141
+<div>{"value":{"a":0,"b":"value1","c":true,"wat0":"joe"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/mutation-within-jsx.tsx
new
+53
@@ -0,0 +1,53 @@
1
+// @enableReactiveScopesInHIR:false
2
+import {
3
+ Stringify,
4
+ makeObject_Primitives,
5
+ mutateAndReturn,
6
+} from "shared-runtime";
7
+
8
+/**
9
+ * In this example, the `<Stringify ... />` JSX block mutates then captures obj.
10
+ * As JSX expressions freeze their values, we know that `obj` and `myDiv` cannot
11
+ * be mutated past this.
12
+ * This set of mutable range + scopes is an edge case because the JSX expression
13
+ * references values in two scopes.
14
+ * - (freeze) the result of `mutateAndReturn`
15
+ * this is a mutable value with a mutable range starting at `makeObject()`
16
+ * - (mutate) the lvalue storing the result of `<Stringify .../>`
17
+ * this is a immutable value and so gets assigned a different scope
18
+ *
19
+ * obj@0 = makeObj(); ⌝ scope@0
20
+ * if (cond) { |
21
+ * $1@0 = mutate(obj@0); |
22
+ * myDiv@1 = JSX $1@0 <- scope@1 |
23
+ * } ⌟
24
+ *
25
+ * Coincidentally, the range of `obj` is extended by alignScopesToBlocks to *past*
26
+ * the end of the JSX instruction. As we currently alias identifier mutableRanges to
27
+ * scope ranges, this `freeze` reference is perceived as occurring during the mutable
28
+ * range of `obj` (even though it is after the last mutating reference).
29
+ *
30
+ * This case is technically safe as `myDiv` correctly takes `obj` as a dependency. As
31
+ * a result, developers can never observe myDiv can aliasing a different value generation
32
+ * than `obj` (e.g. the invariant `myDiv.props.value === obj` always holds).
33
+ */
34
+function useFoo({ data }) {
35
+ let obj = null;
36
+ let myDiv = null;
37
+ if (data.cond) {
38
+ obj = makeObject_Primitives();
39
+ if (data.cond1) {
40
+ myDiv = <Stringify value={mutateAndReturn(obj)} />;
41
+ }
42
+ }
43
+ return myDiv;
44
+}
45
+
46
+export const FIXTURE_ENTRYPOINT = {
47
+ fn: useFoo,
48
+ params: [{ data: { cond: true, cond1: true } }],
49
+ sequentialRenders: [
50
+ { data: { cond: true, cond1: true } },
51
+ { data: { cond: true, cond1: true } },
52
+ ],
53
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/repro-allocating-ternary-test-instruction-scope.expect.md
new
+61
@@ -0,0 +1,61 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableReactiveScopesInHIR:false
6
+import { identity, makeObject_Primitives } from "shared-runtime";
7
+
8
+function useTest({ cond }) {
9
+ const val = makeObject_Primitives();
10
+
11
+ useHook();
12
+ /**
13
+ * We don't technically need a reactive scope for this ternary as
14
+ * it cannot produce newly allocated values.
15
+ * While identity(...) may allocate, we can teach the compiler that
16
+ * its result is only used as as a test condition
17
+ */
18
+ const result = identity(cond) ? val : null;
19
+ return result;
20
+}
21
+
22
+export const FIXTURE_ENTRYPOINT = {
23
+ fn: useTest,
24
+ params: [{ cond: true }],
25
+};
26
+
27
+```
28
+
29
+## Code
30
+
31
+```javascript
32
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableReactiveScopesInHIR:false
33
+import { identity, makeObject_Primitives } from "shared-runtime";
34
+
35
+function useTest(t0) {
36
+ const $ = useMemoCache(1);
37
+ const { cond } = t0;
38
+ let t1;
39
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
40
+ t1 = makeObject_Primitives();
41
+ $[0] = t1;
42
+ } else {
43
+ t1 = $[0];
44
+ }
45
+ const val = t1;
46
+
47
+ useHook();
48
+
49
+ const result = identity(cond) ? val : null;
50
+ return result;
51
+}
52
+
53
+export const FIXTURE_ENTRYPOINT = {
54
+ fn: useTest,
55
+ params: [{ cond: true }],
56
+};
57
+
58
+```
59
+
60
+### Eval output
61
+(kind: exception) useHook is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/original-reactive-scopes-fork/repro-allocating-ternary-test-instruction-scope.ts
new
+21
@@ -0,0 +1,21 @@
1
+// @enableReactiveScopesInHIR:false
2
+import { identity, makeObject_Primitives } from "shared-runtime";
3
+
4
+function useTest({ cond }) {
5
+ const val = makeObject_Primitives();
6
+
7
+ useHook();
8
+ /**
9
+ * We don't technically need a reactive scope for this ternary as
10
+ * it cannot produce newly allocated values.
11
+ * While identity(...) may allocate, we can teach the compiler that
12
+ * its result is only used as as a test condition
13
+ */
14
+ const result = identity(cond) ? val : null;
15
+ return result;
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: useTest,
20
+ params: [{ cond: true }],
21
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-allocating-ternary-test-instruction-scope.expect.md
new
+67
@@ -0,0 +1,67 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { identity, makeObject_Primitives } from "shared-runtime";
6
+
7
+function useTest({ cond }) {
8
+ const val = makeObject_Primitives();
9
+
10
+ useHook();
11
+ /**
12
+ * We don't technically need a reactive scope for this ternary as
13
+ * it cannot produce newly allocated values.
14
+ * While identity(...) may allocate, we can teach the compiler that
15
+ * its result is only used as as a test condition
16
+ */
17
+ const result = identity(cond) ? val : null;
18
+ return result;
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: useTest,
23
+ params: [{ cond: true }],
24
+};
25
+
26
+```
27
+
28
+## Code
29
+
30
+```javascript
31
+import { unstable_useMemoCache as useMemoCache } from "react";
32
+import { identity, makeObject_Primitives } from "shared-runtime";
33
+
34
+function useTest(t0) {
35
+ const $ = useMemoCache(3);
36
+ const { cond } = t0;
37
+ let t1;
38
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
39
+ t1 = makeObject_Primitives();
40
+ $[0] = t1;
41
+ } else {
42
+ t1 = $[0];
43
+ }
44
+ const val = t1;
45
+
46
+ useHook();
47
+ let t2;
48
+ if ($[1] !== cond) {
49
+ t2 = identity(cond) ? val : null;
50
+ $[1] = cond;
51
+ $[2] = t2;
52
+ } else {
53
+ t2 = $[2];
54
+ }
55
+ const result = t2;
56
+ return result;
57
+}
58
+
59
+export const FIXTURE_ENTRYPOINT = {
60
+ fn: useTest,
61
+ params: [{ cond: true }],
62
+};
63
+
64
+```
65
+
66
+### Eval output
67
+(kind: exception) useHook is not defined
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-allocating-ternary-test-instruction-scope.ts
new
+20
@@ -0,0 +1,20 @@
1
+import { identity, makeObject_Primitives } from "shared-runtime";
2
+
3
+function useTest({ cond }) {
4
+ const val = makeObject_Primitives();
5
+
6
+ useHook();
7
+ /**
8
+ * We don't technically need a reactive scope for this ternary as
9
+ * it cannot produce newly allocated values.
10
+ * While identity(...) may allocate, we can teach the compiler that
11
+ * its result is only used as as a test condition
12
+ */
13
+ const result = identity(cond) ? val : null;
14
+ return result;
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: useTest,
19
+ params: [{ cond: true }],
20
+};
compiler/packages/snap/src/SproutTodoFilter.ts
+2
@@ -488,6 +488,8 @@ const skipFilter = new Set([
488
489
// bugs
490
"bug-invalid-reactivity-value-block",
491
+ "original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",
492
+ "original-reactive-scopes-fork/bug-hoisted-declaration-with-scope",
493
494
// 'react-forget-runtime' not yet supported
495
"flag-enable-emit-hook-guards",
compiler/packages/snap/src/sprout/shared-runtime.ts
+7
@@ -314,3 +314,10 @@ export const ObjectWithHooks = {
314
return arg;
315
},
316
};
317
+
318
+export function useFragment(...args: Array<any>): Object {
319
+ return {
320
+ a: [1, 2, 3],
321
+ b: { c: { d: 4 } },
322
+ };
323
+}