@samitouri / QOS-React-2 / commits / 601eb2a23a

Inline useMemo callbacks to allow improved memoization

This is a simplified version of #1454. The goal of this PR is to inline the contents of `useMemo()` callbacks, rather than just immediately invoke the lambda. Turning useMemo() into an IIFE works, but it means that we can't optimize within the lambda block. Our investigations showed that there's a lot of room to optimize at a finer granularity than manually written useMemo calls. For example, one product instance had a useMemo that created a list of child JSX elements. Most of those elements only relied on a single variable (`a`), but a few relied on a second variable (`b). Thus _all_ elements were invalidated whenever `b` changed. If Forget retains the original lambda, we have no choice but to keep that (coarse) granularity for memoization. When we inline, we can optimize to make e.g. individual JSX elements depend on their precise dependencies. The rough idea is: * Keep track of all function expressions * When we find a useMemo, lookup its function expression, and add its CFG to the main function (the previous PR ensures that BlockIds won't collide) * Replace any return statements with a StoreLocal to save the result and a Goto to the code following the useMemo call. * Then we run the usual set of passes to patch the HIR back up again. Example: ```javascript // Before function Component(props) { const x = useMemo(() => { if (props.cond) { return null; } return foo(props.x); }, [props.x]); return x + props.y; } // Intended - **before** memoization function Component(props) { let x; if (props.cond) { x = null; } else { x = foo(props.x); } return x + props.y; } ```

Joe Savona committed Apr 7, 2023 at 16:34 UTC 601eb2a23aa06b46799dfeebaf278d94bb5f298f
16 files changed +626 -35
compiler/forget/src/CompilerPipeline.ts
+4
@@ -21,6 +21,7 @@ import {
21 dropMemoCalls,
22 inferMutableRanges,
23 inferReferenceEffects,
24 + inlineUseMemo,
25 } from "./Inference";
26 import { constantPropagation, deadCodeElimination } from "./Optimization";
27 import {
@@ -60,6 +61,9 @@ export function* run(
61 const hir = lower(func, env).unwrap();
62 yield log({ kind: "hir", name: "HIR", value: hir });
63
64 + inlineUseMemo(hir);
65 + yield log({ kind: "hir", name: "RewriteUseMemo", value: hir });
66 +
67 mergeConsecutiveBlocks(hir);
68 yield log({ kind: "hir", name: "MergeConsecutiveBlocks", value: hir });
69
compiler/forget/src/HIR/MergeConsecutiveBlocks.ts
+2 -1
@@ -13,7 +13,7 @@ import {
13 HIRFunction,
14 Instruction,
15 } from "./HIR";
16 -import { removeUnreachableFallthroughs } from "./HIRBuilder";
16 +import { markPredecessors, removeUnreachableFallthroughs } from "./HIRBuilder";
17
18 /**
19 * Merges sequences of blocks that will always execute consecutively —
@@ -85,6 +85,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
85 merged.merge(block.id, predecessorId);
86 fn.body.blocks.delete(block.id);
87 }
88 + markPredecessors(fn.body);
89 removeUnreachableFallthroughs(fn.body);
90 }
91
compiler/forget/src/Inference/InlineUseMemo.ts new
+343
@@ -0,0 +1,343 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import { CompilerError } from "../CompilerError";
9 +import {
10 + BasicBlock,
11 + BlockId,
12 + Effect,
13 + Environment,
14 + FunctionExpression,
15 + GotoTerminal,
16 + GotoVariant,
17 + HIR,
18 + HIRFunction,
19 + IdentifierId,
20 + InstructionKind,
21 + makeInstructionId,
22 + makeType,
23 + Place,
24 + reversePostorderBlocks,
25 + shrink,
26 +} from "../HIR";
27 +import { markInstructionIds, markPredecessors } from "../HIR/HIRBuilder";
28 +import { assertExhaustive, retainWhere } from "../Utils/utils";
29 +
30 +/**
31 + * Rewrites `useMemo()` calls, rewriting so that the lambda body becomes part of the
32 + * outer block's instructions.
33 + *
34 + * Example:
35 + *
36 + * ```javascript
37 + * // Before
38 + * const x = useMemo(() => foo(y, z), [y, z])
39 + *
40 + * // After
41 + * const x = foo(y, z);
42 + * ```
43 + *
44 + * The main challenge is dealing with the possibility of complex control flow within
45 + * the lambda body. The approach is roughly:
46 + * - split the block with the useMemo call in two:
47 + * - the first block is everything up to the memo call plus the lambda body
48 + * - the second block is everything after the memo call
49 + * - use the temporary from the useMemo call result value as the place to store
50 + * the useMemo result
51 + * - for every return terminal in the lambda body:
52 + * - add a StoreLocal to the temporary, assigning the return value
53 + * - replace the terminal w a goto to the second block
54 + *
55 + * NOTE: *this pass must be run prior to EnterSSA*. Prior to entering SSA form identifiers
56 + * in the top-level function and any function expressions will have consistent
57 + * correlation between `Identifier` instances and IdentifierIds. After entering SSA
58 + * form we drop this correspondence. It's much easier to write this inlining pass
59 + * without having to worry about SSA form.
60 + */
61 +export function inlineUseMemo(fn: HIRFunction): void {
62 + // Track all function expressions in case they appear as the argument to a useMemo
63 + const functions = new Map<IdentifierId, FunctionExpression>();
64 + // Track all references to `useMemo`
65 + const useMemoGlobals = new Set<IdentifierId>();
66 + // Identifiers (lvalues) for known useMemo functions, so that we can prune them
67 + // at the end of the pass
68 + const useMemoFunctions = new Set<IdentifierId>();
69 +
70 + // Iterate the *existing* blocks from the outer component to find useMemo calls
71 + // and inline them. During iteration we will modify `fn` (by inlining the CFG
72 + // of useMemo callbacks) so we explicitly copy references to just the original
73 + // function's blocks first. As blocks are split to make room for useMemo calls,
74 + // the split portions of the blocks will be added to this queue.
75 + const queue = Array.from(fn.body.blocks.values());
76 + queue: for (const block of queue) {
77 + for (let ii = 0; ii < block.instructions.length; ii++) {
78 + const instr = block.instructions[ii]!;
79 + switch (instr.value.kind) {
80 + case "LoadGlobal": {
81 + if (instr.value.name === "useMemo") {
82 + useMemoGlobals.add(instr.lvalue.identifier.id);
83 + }
84 + break;
85 + }
86 + case "FunctionExpression": {
87 + functions.set(instr.lvalue.identifier.id, instr.value);
88 + break;
89 + }
90 + case "CallExpression": {
91 + if (useMemoGlobals.has(instr.value.callee.identifier.id)) {
92 + const [lambda] = instr.value.args;
93 + if (lambda.kind === "Spread") {
94 + continue;
95 + }
96 + const body = functions.get(lambda.identifier.id);
97 + if (body === undefined) {
98 + CompilerError.invariant(
99 + "Expected first argument to useMemo() to be a function expression",
100 + fn.loc
101 + );
102 + }
103 + // We know this function is used for useMemo and can prune it later
104 + useMemoFunctions.add(lambda.identifier.id);
105 +
106 + // Create a new block which will contain code following the useMemo call
107 + const continuationBlockId = fn.env.nextBlockId;
108 + const continuationBlock: BasicBlock = {
109 + id: continuationBlockId,
110 + instructions: block.instructions.slice(ii + 1),
111 + kind: block.kind,
112 + phis: new Set(),
113 + preds: new Set(),
114 + terminal: block.terminal,
115 + };
116 + fn.body.blocks.set(continuationBlockId, continuationBlock);
117 +
118 + // Trim the original block to contain instructions up to (but not including)
119 + // the useMemo
120 + block.instructions.length = ii;
121 +
122 + // The block leading up to the useMemo needs to jump to the entry block of
123 + // the useMemo control flow graph. These will be merged into a single block
124 + // via MergeConsectuveBlocks
125 + const newTerminal: GotoTerminal = {
126 + block: body.loweredFunc.body.entry,
127 + id: makeInstructionId(0),
128 + kind: "goto",
129 + variant: GotoVariant.Break,
130 + loc: block.terminal.loc,
131 + };
132 + block.terminal = newTerminal;
133 +
134 + // If the final terminal type has a fallthrough, update it to point to the
135 + // continuation block
136 + const terminalBlock = getTerminalBlock(
137 + body.loweredFunc.body,
138 + body.loweredFunc.body.entry
139 + );
140 + switch (terminalBlock.terminal.kind) {
141 + case "if":
142 + case "switch":
143 + case "label": {
144 + // These terminals can all appear as the final top-level terminal
145 + // *and* have fallthroughs. If they are final, their fallthrough
146 + // must be updated to point to the continuation block to main
147 + // proper CFG structure (a block that succeeds all branches of a conditional
148 + // must be marked as that conditional's fallthrough)
149 + terminalBlock.terminal.fallthrough = continuationBlockId;
150 + break;
151 + }
152 + case "return":
153 + case "throw": {
154 + // These can appear as the final top-level terminal
155 + break;
156 + }
157 + // These all have non-nullable fallthroughs: there is always some code in the
158 + // CFG that succeeds them which we should find instead
159 + case "optional-call":
160 + case "ternary":
161 + case "logical":
162 + case "while":
163 + case "for":
164 + case "for-of":
165 + case "do-while":
166 + // These are invalid terminals for a top-level block
167 + case "branch":
168 + case "goto":
169 + case "unsupported": {
170 + CompilerError.invariant(
171 + `Unexpected final top-level terminal`,
172 + terminalBlock.terminal.loc,
173 + `Found ${terminalBlock.terminal.kind}, expected one of if, switch, label, return, or throw`
174 + );
175 + }
176 + default: {
177 + assertExhaustive(
178 + terminalBlock.terminal,
179 + `Unexpected terminal kind '${
180 + (terminalBlock.terminal as any).kind
181 + }'`
182 + );
183 + }
184 + }
185 +
186 + // Rewrite blocks from the lambda to replace any `return` with a
187 + // store the useMemo temporary and `goto` the continuation block
188 + for (const [id, block] of body.loweredFunc.body.blocks) {
189 + block.preds.clear();
190 + rewriteBlock(fn.env, block, continuationBlockId, instr.lvalue);
191 + fn.body.blocks.set(id, block);
192 + }
193 +
194 + // Ensure we visit the continuation block, since there may have been
195 + // sequential useMemos that need to be visited.
196 + queue.push(continuationBlock);
197 + continue queue;
198 + }
199 + }
200 + }
201 + }
202 + }
203 +
204 + if (useMemoFunctions.size !== 0) {
205 + // Remove instructions that define lambdas which we inlined
206 + for (const [, block] of fn.body.blocks) {
207 + retainWhere(
208 + block.instructions,
209 + (instr) => !useMemoFunctions.has(instr.lvalue.identifier.id)
210 + );
211 + }
212 +
213 + // If terminals have changed then blocks may have become newly unreachable.
214 + // Re-run minification of the graph (incl reordering instruction ids)
215 + shrink(fn.body);
216 + reversePostorderBlocks(fn.body);
217 + markInstructionIds(fn.body);
218 + markPredecessors(fn.body);
219 + }
220 +}
221 +
222 +// Finds the final top-level terminal node for a CFG, by following any
223 +// fallthrough nodes.
224 +function getTerminalBlock(cfg: HIR, start: BlockId): BasicBlock {
225 + let current = cfg.blocks.get(start)!;
226 + while (true) {
227 + const { terminal } = current;
228 + switch (terminal.kind) {
229 + case "if": {
230 + if (
231 + terminal.fallthrough !== null &&
232 + terminal.fallthrough === terminal.alternate
233 + ) {
234 + // Here we don't know if the fallthrough and alternate are the same because there was
235 + // no alternate or because both the alternate exists and the fallthrough is just unreachable
236 + // So we check if the fallthrough returns/throws (the if is the final top-level terminal)
237 + // or whether execution actually may continue.
238 + const fallthrough = getTerminalBlock(cfg, terminal.fallthrough);
239 + if (
240 + fallthrough.terminal.kind === "return" ||
241 + fallthrough.terminal.kind === "throw"
242 + ) {
243 + return current;
244 + } else {
245 + current = fallthrough;
246 + continue;
247 + }
248 + } else {
249 + return current;
250 + }
251 + }
252 + case "switch":
253 + case "label": {
254 + if (terminal.fallthrough !== null) {
255 + current = cfg.blocks.get(terminal.fallthrough)!;
256 + continue;
257 + } else {
258 + return current;
259 + }
260 + }
261 + case "optional-call":
262 + case "ternary":
263 + case "logical":
264 + case "while":
265 + case "for":
266 + case "for-of":
267 + case "do-while": {
268 + current = cfg.blocks.get(terminal.fallthrough)!;
269 + continue;
270 + }
271 + case "return":
272 + case "throw": {
273 + return current;
274 + }
275 + case "unsupported":
276 + case "branch":
277 + case "goto": {
278 + CompilerError.invariant(
279 + `Unexpected block terminal`,
280 + terminal.loc,
281 + `Top-level blocks may not end in a ${terminal.kind} terminal`
282 + );
283 + }
284 + default: {
285 + assertExhaustive(
286 + terminal,
287 + `Unexpected terminal kind '${(terminal as any).kind}'`
288 + );
289 + }
290 + }
291 + }
292 +}
293 +
294 +/**
295 + * Rewrites the block so that all `return` terminals are replaced:
296 + * * Add a StoreLocal <returnValue> = <terminal.value>
297 + * * Replace the terminal with a Goto to <returnTarget>
298 + */
299 +function rewriteBlock(
300 + env: Environment,
301 + block: BasicBlock,
302 + returnTarget: BlockId,
303 + returnValue: Place
304 +): void {
305 + const { terminal } = block;
306 + if (terminal.kind !== "return") {
307 + return;
308 + }
309 + if (terminal.value !== null) {
310 + block.instructions.push({
311 + id: makeInstructionId(0),
312 + loc: terminal.loc,
313 + lvalue: {
314 + effect: Effect.Unknown,
315 + identifier: {
316 + id: env.nextIdentifierId,
317 + mutableRange: {
318 + start: makeInstructionId(0),
319 + end: makeInstructionId(0),
320 + },
321 + name: null,
322 + scope: null,
323 + type: makeType(),
324 + },
325 + kind: "Identifier",
326 + loc: terminal.loc,
327 + },
328 + value: {
329 + kind: "StoreLocal",
330 + lvalue: { kind: InstructionKind.Const, place: { ...returnValue } },
331 + value: terminal.value,
332 + loc: terminal.loc,
333 + },
334 + });
335 + }
336 + block.terminal = {
337 + kind: "goto",
338 + block: returnTarget,
339 + id: makeInstructionId(0),
340 + variant: GotoVariant.Break,
341 + loc: block.terminal.loc,
342 + };
343 +}
compiler/forget/src/Inference/index.ts
+2 -1
@@ -5,7 +5,8 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +export { default as analyseFunctions } from "./AnalyseFunctions";
9 export { default as dropMemoCalls } from "./DropMemoCalls";
10 export { inferMutableRanges } from "./InferMutableRanges";
10 -export { default as analyseFunctions } from "./AnalyseFunctions";
11 export { default as inferReferenceEffects } from "./InferReferenceEffects";
12 +export { inlineUseMemo } from "./InlineUseMemo";
compiler/forget/src/__tests__/fixtures/compiler/useMemo-if-else-multiple-return.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = useMemo(() => {
7 + if (props.cond) {
8 + return makeObject(props.a);
9 + }
10 + return makeObject(props.b);
11 + });
12 + return x;
13 +}
14 +
15 +```
16 +
17 +## Code
18 +
19 +```javascript
20 +function Component(props) {
21 + const $ = React.unstable_useMemoCache(5);
22 + if (props.cond) {
23 + const c_0 = $[0] !== props.a;
24 + let t0;
25 + if (c_0) {
26 + t0 = makeObject(props.a);
27 + $[0] = props.a;
28 + $[1] = t0;
29 + } else {
30 + t0 = $[1];
31 + }
32 + let t1;
33 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
34 + t1 = t0;
35 + $[2] = t1;
36 + } else {
37 + t1 = $[2];
38 + }
39 + } else {
40 + const c_3 = $[3] !== props.b;
41 + let t2;
42 + if (c_3) {
43 + t2 = makeObject(props.b);
44 + $[3] = props.b;
45 + $[4] = t2;
46 + } else {
47 + t2 = $[4];
48 + }
49 + t1 = t2;
50 + }
51 + const x = t1;
52 + return x;
53 +}
54 +
55 +```
56 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/useMemo-if-else-multiple-return.js new
+9
@@ -0,0 +1,9 @@
1 +function Component(props) {
2 + const x = useMemo(() => {
3 + if (props.cond) {
4 + return makeObject(props.a);
5 + }
6 + return makeObject(props.b);
7 + });
8 + return x;
9 +}
compiler/forget/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md
+26 -17
@@ -18,43 +18,52 @@ function Component(props) {
18
19 ```javascript
20 function Component(props) {
21 - const $ = React.unstable_useMemoCache(7);
21 + const $ = React.unstable_useMemoCache(10);
22 const c_0 = $[0] !== props.a;
23 let t0;
24 if (c_0) {
25 - t0 = () => {
26 - const items = [];
27 - const a = makeObject(props.a);
28 - const b = makeObject(props.b);
29 - return [a, b];
30 - };
25 + t0 = makeObject(props.a);
26 $[0] = props.a;
27 $[1] = t0;
28 } else {
29 t0 = $[1];
30 }
36 - const c_2 = $[2] !== t0;
31 + const a = t0;
32 + const c_2 = $[2] !== props.b;
33 let t1;
34 if (c_2) {
39 - t1 = t0();
40 - $[2] = t0;
35 + t1 = makeObject(props.b);
36 + $[2] = props.b;
37 $[3] = t1;
38 } else {
39 t1 = $[3];
40 }
45 - const [a_0, b_0] = t1;
46 - const c_4 = $[4] !== a_0;
47 - const c_5 = $[5] !== b_0;
41 + const b = t1;
42 + const c_4 = $[4] !== a;
43 + const c_5 = $[5] !== b;
44 let t2;
45 if (c_4 || c_5) {
50 - t2 = [a_0, b_0];
51 - $[4] = a_0;
52 - $[5] = b_0;
46 + t2 = [a, b];
47 + $[4] = a;
48 + $[5] = b;
49 $[6] = t2;
50 } else {
51 t2 = $[6];
52 }
57 - return t2;
53 + const t54 = t2;
54 + const [a_0, b_0] = t54;
55 + const c_7 = $[7] !== a_0;
56 + const c_8 = $[8] !== b_0;
57 + let t3;
58 + if (c_7 || c_8) {
59 + t3 = [a_0, b_0];
60 + $[7] = a_0;
61 + $[8] = b_0;
62 + $[9] = t3;
63 + } else {
64 + t3 = $[9];
65 + }
66 + return t3;
67 }
68
69 ```
compiler/forget/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md new
+26
@@ -0,0 +1,26 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = useMemo(() => {
7 + label: {
8 + return props.value;
9 + }
10 + });
11 + return x;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +function Component(props) {
20 + const t19 = props.value;
21 + const x = t19;
22 + return x;
23 +}
24 +
25 +```
26 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.js new
+8
@@ -0,0 +1,8 @@
1 +function Component(props) {
2 + const x = useMemo(() => {
3 + label: {
4 + return props.value;
5 + }
6 + });
7 + return x;
8 +}
compiler/forget/src/__tests__/fixtures/compiler/useMemo-logical.expect.md new
+22
@@ -0,0 +1,22 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = useMemo(() => props.a && props.b);
7 + return x;
8 +}
9 +
10 +```
11 +
12 +## Code
13 +
14 +```javascript
15 +function Component(props) {
16 + const t32 = props.a && props.b;
17 + const x = t32;
18 + return x;
19 +}
20 +
21 +```
22 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/useMemo-logical.js new
+4
@@ -0,0 +1,4 @@
1 +function Component(props) {
2 + const x = useMemo(() => props.a && props.b);
3 + return x;
4 +}
compiler/forget/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = useMemo(() => {
7 + let y = [];
8 + if (props.cond) {
9 + y.push(props.a);
10 + }
11 + if (props.cond2) {
12 + return y;
13 + }
14 + y.push(props.b);
15 + return y;
16 + });
17 + return x;
18 +}
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +function Component(props) {
26 + const $ = React.unstable_useMemoCache(2);
27 + const c_0 = $[0] !== props;
28 + let t0;
29 + if (c_0) {
30 + const y = [];
31 + if (props.cond) {
32 + y.push(props.a);
33 + }
34 + if (props.cond2) {
35 + t0 = y;
36 + } else {
37 + y.push(props.b);
38 + t0 = y;
39 + }
40 + $[0] = props;
41 + $[1] = t0;
42 + } else {
43 + t0 = $[1];
44 + }
45 + const x = t0;
46 + return x;
47 +}
48 +
49 +```
50 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.js new
+14
@@ -0,0 +1,14 @@
1 +function Component(props) {
2 + const x = useMemo(() => {
3 + let y = [];
4 + if (props.cond) {
5 + y.push(props.a);
6 + }
7 + if (props.cond2) {
8 + return y;
9 + }
10 + y.push(props.b);
11 + return y;
12 + });
13 + return x;
14 +}
compiler/forget/src/__tests__/fixtures/compiler/useMemo-simple.expect.md
+8 -16
@@ -13,36 +13,28 @@ function component(a) {
13
14 ```javascript
15 function component(a) {
16 - const $ = React.unstable_useMemoCache(6);
16 + const $ = React.unstable_useMemoCache(4);
17 const c_0 = $[0] !== a;
18 let t0;
19 if (c_0) {
20 - t0 = () => [a];
20 + t0 = [a];
21 $[0] = a;
22 $[1] = t0;
23 } else {
24 t0 = $[1];
25 }
26 - const c_2 = $[2] !== t0;
26 + const t23 = t0;
27 + const x = t23;
28 + const c_2 = $[2] !== x;
29 let t1;
30 if (c_2) {
29 - t1 = t0();
30 - $[2] = t0;
31 + t1 = <Foo x={x} />;
32 + $[2] = x;
33 $[3] = t1;
34 } else {
35 t1 = $[3];
36 }
35 - const x = t1;
36 - const c_4 = $[4] !== x;
37 - let t2;
38 - if (c_4) {
39 - t2 = <Foo x={x} />;
40 - $[4] = x;
41 - $[5] = t2;
42 - } else {
43 - t2 = $[5];
44 - }
45 - return t2;
37 + return t1;
38 }
39
40 ```
compiler/forget/src/__tests__/fixtures/compiler/useMemo-switch-no-fallthrough.expect.md new
+39
@@ -0,0 +1,39 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const x = useMemo(() => {
7 + switch (props.key) {
8 + case "key": {
9 + return props.value;
10 + }
11 + default: {
12 + return props.defaultValue;
13 + }
14 + }
15 + });
16 + return x;
17 +}
18 +
19 +```
20 +
21 +## Code
22 +
23 +```javascript
24 +function Component(props) {
25 + bb8: switch (props.key) {
26 + case "key": {
27 + const t28 = props.value;
28 + break bb8;
29 + }
30 + default: {
31 + const t28 = props.defaultValue;
32 + }
33 + }
34 + const x = t28;
35 + return x;
36 +}
37 +
38 +```
39 +
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/useMemo-switch-no-fallthrough.js new
+13
@@ -0,0 +1,13 @@
1 +function Component(props) {
2 + const x = useMemo(() => {
3 + switch (props.key) {
4 + case "key": {
5 + return props.value;
6 + }
7 + default: {
8 + return props.defaultValue;
9 + }
10 + }
11 + });
12 + return x;
13 +}