@samitouri / QOS-React / commits / 6db5f36aa7

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