@samitouri / QOS-React-1 / commits / 57fcf7d305

Repro for nonproblematic unreachable code

This case is specific to early return inside an inlined IIFE (which can often occur as a result of dropping manual memoization). When we inline IIFEs, as a reminder we wrap the body in a labeled block and convert returns to assignment of a temporary + break out of the label. Those reassignments themselves are getting a reactive scope assigned since the reassigned value has a mutable range. They don't really need a mutable range or scope, though. And then the presence of the `break` statements means that we can sometimes exit out of the scope before reaching the end - leading to unreachable code. This can only occur though where _all the values are already memoized_. So the code works just fine and even memoizes just fine - it's just that we have some extraneous scopes and there is technically unreachable code. I'll fix in a follow-up, adding a repro here.

Joe Savona committed Mar 19, 2024 at 13:28 UTC 57fcf7d305125784b5b98dd740cdd8a3a4125304
2 files changed +218
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unreachable-code-early-return-in-useMemo.expect.md new
+163
@@ -0,0 +1,163 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions
6 +import { useMemo, useState } from "react";
7 +import { Stringify, identity } from "shared-runtime";
8 +
9 +function Component({ value }) {
10 + "use no forget";
11 + const result = useValue(value);
12 + return <Validate inputs={[value]} output={result} />;
13 +}
14 +
15 +function Validate({ inputs, output }) {
16 + "use no forget";
17 + const [previousInputs, setPreviousInputs] = useState(inputs);
18 + const [previousOutput, setPreviousOutput] = useState(output);
19 + if (
20 + inputs.length !== previousInputs.length ||
21 + inputs.some((item, i) => item !== previousInputs[i])
22 + ) {
23 + // Some input changed, we expect the output to change
24 + setPreviousInputs(inputs);
25 + setPreviousOutput(output);
26 + } else if (output !== previousOutput) {
27 + // Else output should be stable
28 + throw new Error("Output identity changed but inputs did not");
29 + }
30 + return <Stringify inputs={inputs} output={output} />;
31 +}
32 +
33 +function useValue(value) {
34 + return useMemo(() => {
35 + if (value == null) {
36 + return null;
37 + }
38 + try {
39 + return { value };
40 + } catch (e) {
41 + return null;
42 + }
43 + }, [value]);
44 +}
45 +
46 +export const FIXTURE_ENTRYPOINT = {
47 + fn: Component,
48 + params: [{ value: null }],
49 + sequentialRenders: [
50 + { value: null },
51 + { value: null },
52 + { value: 42 },
53 + { value: 42 },
54 + { value: null },
55 + { value: 42 },
56 + { value: null },
57 + { value: 42 },
58 + ],
59 +};
60 +
61 +```
62 +
63 +## Code
64 +
65 +```javascript
66 +// @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions
67 +import {
68 + useMemo,
69 + useState,
70 + unstable_useMemoCache as useMemoCache,
71 +} from "react";
72 +import { Stringify, identity } from "shared-runtime";
73 +
74 +function Component({ value }) {
75 + "use no forget";
76 + const result = useValue(value);
77 + return <Validate inputs={[value]} output={result} />;
78 +}
79 +
80 +function Validate({ inputs, output }) {
81 + "use no forget";
82 + const [previousInputs, setPreviousInputs] = useState(inputs);
83 + const [previousOutput, setPreviousOutput] = useState(output);
84 + if (
85 + inputs.length !== previousInputs.length ||
86 + inputs.some((item, i) => item !== previousInputs[i])
87 + ) {
88 + // Some input changed, we expect the output to change
89 + setPreviousInputs(inputs);
90 + setPreviousOutput(output);
91 + } else if (output !== previousOutput) {
92 + // Else output should be stable
93 + throw new Error("Output identity changed but inputs did not");
94 + }
95 + return <Stringify inputs={inputs} output={output} />;
96 +}
97 +
98 +function useValue(value) {
99 + const $ = useMemoCache(5);
100 + let t0;
101 + bb13: {
102 + if (value == null) {
103 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
104 + t0 = null;
105 + break bb13;
106 + $[0] = t0;
107 + } else {
108 + t0 = $[0];
109 + }
110 + }
111 + try {
112 + let t2;
113 + if ($[1] !== value) {
114 + t2 = { value };
115 + $[1] = value;
116 + $[2] = t2;
117 + } else {
118 + t2 = $[2];
119 + }
120 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
121 + t0 = t2;
122 + $[3] = t0;
123 + } else {
124 + t0 = $[3];
125 + }
126 + } catch (t1) {
127 + if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
128 + t0 = null;
129 + $[4] = t0;
130 + } else {
131 + t0 = $[4];
132 + }
133 + }
134 + }
135 + return t0;
136 +}
137 +
138 +export const FIXTURE_ENTRYPOINT = {
139 + fn: Component,
140 + params: [{ value: null }],
141 + sequentialRenders: [
142 + { value: null },
143 + { value: null },
144 + { value: 42 },
145 + { value: 42 },
146 + { value: null },
147 + { value: 42 },
148 + { value: null },
149 + { value: 42 },
150 + ],
151 +};
152 +
153 +```
154 +
155 +### Eval output
156 +(kind: ok) <div>{"inputs":[null],"output":"[[ cyclic ref *2 ]]"}</div>
157 +<div>{"inputs":[null],"output":"[[ cyclic ref *2 ]]"}</div>
158 +<div>{"inputs":[42],"output":{"value":42}}</div>
159 +<div>{"inputs":[42],"output":{"value":42}}</div>
160 +<div>{"inputs":[null],"output":"[[ cyclic ref *2 ]]"}</div>
161 +<div>{"inputs":[42],"output":{"value":42}}</div>
162 +<div>{"inputs":[null],"output":"[[ cyclic ref *2 ]]"}</div>
163 +<div>{"inputs":[42],"output":{"value":42}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unreachable-code-early-return-in-useMemo.js new
+55
@@ -0,0 +1,55 @@
1 +// @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions
2 +import { useMemo, useState } from "react";
3 +import { Stringify, identity } from "shared-runtime";
4 +
5 +function Component({ value }) {
6 + "use no forget";
7 + const result = useValue(value);
8 + return <Validate inputs={[value]} output={result} />;
9 +}
10 +
11 +function Validate({ inputs, output }) {
12 + "use no forget";
13 + const [previousInputs, setPreviousInputs] = useState(inputs);
14 + const [previousOutput, setPreviousOutput] = useState(output);
15 + if (
16 + inputs.length !== previousInputs.length ||
17 + inputs.some((item, i) => item !== previousInputs[i])
18 + ) {
19 + // Some input changed, we expect the output to change
20 + setPreviousInputs(inputs);
21 + setPreviousOutput(output);
22 + } else if (output !== previousOutput) {
23 + // Else output should be stable
24 + throw new Error("Output identity changed but inputs did not");
25 + }
26 + return <Stringify inputs={inputs} output={output} />;
27 +}
28 +
29 +function useValue(value) {
30 + return useMemo(() => {
31 + if (value == null) {
32 + return null;
33 + }
34 + try {
35 + return { value };
36 + } catch (e) {
37 + return null;
38 + }
39 + }, [value]);
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Component,
44 + params: [{ value: null }],
45 + sequentialRenders: [
46 + { value: null },
47 + { value: null },
48 + { value: 42 },
49 + { value: 42 },
50 + { value: null },
51 + { value: 42 },
52 + { value: null },
53 + { value: 42 },
54 + ],
55 +};