Dramatically simplify InlineUseMemo
I realized a wayyyy simpler approach to inlining a lambda: wrap it in a labeled block. The transformation is roughly as follows: ```javascript // Before const x = useMemo(() => { if (a) { return b; } return c; }, [a, b, c]); return x; // After let x; label: { if (a) { x = b; break label; } x = c; break label; } return x; ``` The key to making this work is fixing up some edge cases in labeled blocks, hence the previous PRs.
Joe Savona committed
May 17, 2023 at 15:55 UTC
329809de818366cfccf0a6a69e00191b2639efeb
5 files changed
+65
-179
compiler/forget/src/Inference/InlineUseMemo.ts
+8
-133
@@ -13,20 +13,19 @@ import {
13
Environment,
14
FunctionExpression,
15
GeneratedSource,
16
- GotoTerminal,
16
GotoVariant,
18
- HIR,
17
HIRFunction,
18
Identifier,
19
IdentifierId,
20
InstructionKind,
21
+ LabelTerminal,
22
Place,
23
makeInstructionId,
24
makeType,
25
reversePostorderBlocks,
26
} from "../HIR";
27
import { markInstructionIds, markPredecessors } from "../HIR/HIRBuilder";
29
-import { assertExhaustive, retainWhere } from "../Utils/utils";
28
+import { retainWhere } from "../Utils/utils";
29
30
/**
31
* Rewrites `useMemo()` calls, rewriting so that the lambda body becomes part of the
@@ -133,70 +132,18 @@ export function inlineUseMemo(fn: HIRFunction): void {
132
// the useMemo
133
block.instructions.length = ii;
134
136
- // The block leading up to the useMemo needs to jump to the entry block of
137
- // the useMemo control flow graph. These will be merged into a single block
138
- // via MergeConsectuveBlocks
139
- const newTerminal: GotoTerminal = {
135
+ // To account for complex control flow within the lambda, we treat the lambda
136
+ // as if it were a single labeled statement, and replace all returns with gotos
137
+ // to the label fallthrough.
138
+ const newTerminal: LabelTerminal = {
139
block: body.loweredFunc.body.entry,
140
id: makeInstructionId(0),
142
- kind: "goto",
143
- variant: GotoVariant.Break,
141
+ kind: "label",
142
+ fallthrough: continuationBlockId,
143
loc: block.terminal.loc,
144
};
145
block.terminal = newTerminal;
146
148
- // If the final terminal type has a fallthrough, update it to point to the
149
- // continuation block
150
- const terminalBlock = getTerminalBlock(
151
- body.loweredFunc.body,
152
- body.loweredFunc.body.entry
153
- );
154
- switch (terminalBlock.terminal.kind) {
155
- case "if":
156
- case "switch":
157
- case "label": {
158
- // These terminals can all appear as the final top-level terminal
159
- // *and* have fallthroughs. If they are final, their fallthrough
160
- // must be updated to point to the continuation block to main
161
- // proper CFG structure (a block that succeeds all branches of a conditional
162
- // must be marked as that conditional's fallthrough)
163
- terminalBlock.terminal.fallthrough = continuationBlockId;
164
- break;
165
- }
166
- case "return":
167
- case "throw": {
168
- // These can appear as the final top-level terminal
169
- break;
170
- }
171
- // These all have non-nullable fallthroughs: there is always some code in the
172
- // CFG that succeeds them which we should find instead
173
- case "optional":
174
- case "ternary":
175
- case "logical":
176
- case "while":
177
- case "for":
178
- case "for-of":
179
- case "do-while":
180
- // These are invalid terminals for a top-level block
181
- case "branch":
182
- case "goto":
183
- case "unsupported": {
184
- CompilerError.invariant(
185
- `Unexpected final top-level terminal`,
186
- terminalBlock.terminal.loc,
187
- `Found ${terminalBlock.terminal.kind}, expected one of if, switch, label, return, or throw`
188
- );
189
- }
190
- default: {
191
- assertExhaustive(
192
- terminalBlock.terminal,
193
- `Unexpected terminal kind '${
194
- (terminalBlock.terminal as any).kind
195
- }'`
196
- );
197
- }
198
- }
199
-
147
// We store the result in the useMemo temporary
148
const result = instr.lvalue;
149
@@ -241,78 +188,6 @@ export function inlineUseMemo(fn: HIRFunction): void {
188
}
189
}
190
244
-// Finds the final top-level terminal node for a CFG, by following any
245
-// fallthrough nodes.
246
-function getTerminalBlock(cfg: HIR, start: BlockId): BasicBlock {
247
- let current = cfg.blocks.get(start)!;
248
- while (true) {
249
- const { terminal } = current;
250
- switch (terminal.kind) {
251
- case "if": {
252
- if (
253
- terminal.fallthrough !== null &&
254
- terminal.fallthrough === terminal.alternate
255
- ) {
256
- // Here we don't know if the fallthrough and alternate are the same because there was
257
- // no alternate or because both the alternate exists and the fallthrough is just unreachable
258
- // So we check if the fallthrough returns/throws (the if is the final top-level terminal)
259
- // or whether execution actually may continue.
260
- const fallthrough = getTerminalBlock(cfg, terminal.fallthrough);
261
- if (
262
- fallthrough.terminal.kind === "return" ||
263
- fallthrough.terminal.kind === "throw"
264
- ) {
265
- return current;
266
- } else {
267
- current = fallthrough;
268
- continue;
269
- }
270
- } else {
271
- return current;
272
- }
273
- }
274
- case "switch":
275
- case "label": {
276
- if (terminal.fallthrough !== null) {
277
- current = cfg.blocks.get(terminal.fallthrough)!;
278
- continue;
279
- } else {
280
- return current;
281
- }
282
- }
283
- case "optional":
284
- case "ternary":
285
- case "logical":
286
- case "while":
287
- case "for":
288
- case "for-of":
289
- case "do-while": {
290
- current = cfg.blocks.get(terminal.fallthrough)!;
291
- continue;
292
- }
293
- case "return":
294
- case "throw": {
295
- return current;
296
- }
297
- case "unsupported":
298
- case "branch":
299
- case "goto": {
300
- CompilerError.invariant(
301
- `Unexpected block terminal`,
302
- terminal.loc,
303
- `Top-level blocks may not end in a ${terminal.kind} terminal`
304
- );
305
- }
306
- default: {
307
- assertExhaustive(
308
- terminal,
309
- `Unexpected terminal kind '${(terminal as any).kind}'`
310
- );
311
- }
312
- }
313
- }
314
-}
315
-
191
/**
192
* Rewrites the block so that all `return` terminals are replaced:
193
* * Add a StoreLocal <returnValue> = <terminal.value>
compiler/forget/src/__tests__/fixtures/compiler/useMemo-if-else-multiple-return.expect.md
+13
-11
@@ -21,18 +21,20 @@ import { unstable_useMemoCache as useMemoCache } from "react";
21
function Component(props) {
22
const $ = useMemoCache(4);
23
let t21 = undefined;
24
- if (props.cond) {
25
- const c_0 = $[0] !== props.a;
26
- let t0;
27
- if (c_0) {
28
- t0 = makeObject(props.a);
29
- $[0] = props.a;
30
- $[1] = t0;
31
- } else {
32
- t0 = $[1];
24
+ bb7: {
25
+ if (props.cond) {
26
+ const c_0 = $[0] !== props.a;
27
+ let t0;
28
+ if (c_0) {
29
+ t0 = makeObject(props.a);
30
+ $[0] = props.a;
31
+ $[1] = t0;
32
+ } else {
33
+ t0 = $[1];
34
+ }
35
+ t21 = t0;
36
+ break bb7;
37
}
34
- t21 = t0;
35
- } else {
38
const c_2 = $[2] !== props.b;
39
let t1;
40
if (c_2) {
compiler/forget/src/__tests__/fixtures/compiler/useMemo-inlining-block-return.expect.md
+13
-11
@@ -20,18 +20,20 @@ import { unstable_useMemoCache as useMemoCache } from "react";
20
function component(a, b) {
21
const $ = useMemoCache(2);
22
let t14 = undefined;
23
- if (a) {
24
- const c_0 = $[0] !== b;
25
- let t0;
26
- if (c_0) {
27
- t0 = { b };
28
- $[0] = b;
29
- $[1] = t0;
30
- } else {
31
- t0 = $[1];
23
+ bb6: {
24
+ if (a) {
25
+ const c_0 = $[0] !== b;
26
+ let t0;
27
+ if (c_0) {
28
+ t0 = { b };
29
+ $[0] = b;
30
+ $[1] = t0;
31
+ } else {
32
+ t0 = $[1];
33
+ }
34
+ t14 = t0;
35
+ break bb6;
36
}
33
- t14 = t0;
34
- } else {
37
t14 = undefined;
38
}
39
const x = t14;
compiler/forget/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md
+22
-17
@@ -24,25 +24,30 @@ function Component(props) {
24
```javascript
25
import { unstable_useMemoCache as useMemoCache } from "react";
26
function Component(props) {
27
- const $ = useMemoCache(2);
28
- const c_0 = $[0] !== props;
29
- let t32;
30
- if (c_0) {
31
- const y = [];
32
- if (props.cond) {
33
- y.push(props.a);
34
- }
35
- t32 = undefined;
36
- if (props.cond2) {
37
- t32 = y;
38
- } else {
27
+ const $ = useMemoCache(3);
28
+ let t32 = undefined;
29
+ bb9: {
30
+ const c_0 = $[0] !== props;
31
+ let y;
32
+ if (c_0) {
33
+ y = [];
34
+ if (props.cond) {
35
+ y.push(props.a);
36
+ }
37
+ if (props.cond2) {
38
+ t32 = y;
39
+ break bb9;
40
+ }
41
+
42
y.push(props.b);
40
- t32 = y;
43
+ $[0] = props;
44
+ $[1] = y;
45
+ $[2] = t32;
46
+ } else {
47
+ y = $[1];
48
+ t32 = $[2];
49
}
42
- $[0] = props;
43
- $[1] = t32;
44
- } else {
45
- t32 = $[1];
50
+ t32 = y;
51
}
52
const x = t32;
53
return x;
compiler/forget/src/__tests__/fixtures/compiler/useMemo-switch-no-fallthrough.expect.md
+9
-7
@@ -23,13 +23,15 @@ function Component(props) {
23
```javascript
24
function Component(props) {
25
let t18 = undefined;
26
- bb8: switch (props.key) {
27
- case "key": {
28
- t18 = props.value;
29
- break bb8;
30
- }
31
- default: {
32
- t18 = props.defaultValue;
26
+ bb8: {
27
+ switch (props.key) {
28
+ case "key": {
29
+ t18 = props.value;
30
+ break bb8;
31
+ }
32
+ default: {
33
+ t18 = props.defaultValue;
34
+ }
35
}
36
}
37
const x = t18;