TryStatement: handle edge case control flows
Adds handling for some cases where the handler is unreachable (or is provably unreachable after analysis & optimization), where the try/catch can be flattened away: * The try block is empty. Nothing can throw, so the handler is unreachable. * The try block will always return. It can't return anything interesting (ie the result of a function call or variable load) since those could throw, but a try block that always return a primitive means the handler is unreachable. * The same, except where we only determine that the try block always returns via constant propagation.
Joe Savona committed
Sep 8, 2023 at 13:54 UTC
de2b684bc5864ff8792a26d0c044731e6586ce13
11 files changed
+226
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+3
@@ -137,6 +137,9 @@ export function* run(
137
deadCodeElimination(hir);
138
yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
139
140
+ pruneMaybeThrows(hir);
141
+ yield log({ kind: "hir", name: "PruneMaybeThrows", value: hir });
142
+
143
inferMutableRanges(hir);
144
yield log({ kind: "hir", name: "InferMutableRanges", value: hir });
145
compiler/packages/babel-plugin-react-forget/src/HIR/HIRBuilder.ts
+22
@@ -316,6 +316,7 @@ export default class HIRBuilder {
316
removeUnreachableForUpdates(ir);
317
removeUnreachableFallthroughs(ir);
318
removeDeadDoWhileStatements(ir);
319
+ removeUnnecessaryTryCatch(ir);
320
markInstructionIds(ir);
321
markPredecessors(ir);
322
@@ -846,3 +847,24 @@ function getTargetIfIndirection(block: BasicBlock): number | null {
847
? block.terminal.block
848
: null;
849
}
850
+
851
+/**
852
+ * Finds try terminals where the handler is unreachable, and converts the try
853
+ * to a goto(terminal.fallthrough)
854
+ */
855
+export function removeUnnecessaryTryCatch(fn: HIR): void {
856
+ for (const [, block] of fn.blocks) {
857
+ if (
858
+ block.terminal.kind === "try" &&
859
+ !fn.blocks.has(block.terminal.handler)
860
+ ) {
861
+ block.terminal = {
862
+ kind: "goto",
863
+ block: block.terminal.block,
864
+ id: makeInstructionId(0),
865
+ loc: block.terminal.loc,
866
+ variant: GotoVariant.Break,
867
+ };
868
+ }
869
+ }
870
+}
compiler/packages/babel-plugin-react-forget/src/HIR/index.ts
+1
@@ -15,6 +15,7 @@ export * from "./HIR";
15
export {
16
markInstructionIds,
17
markPredecessors,
18
+ removeUnnecessaryTryCatch,
19
removeUnreachableFallthroughs,
20
reversePostorderBlocks,
21
} from "./HIRBuilder";
compiler/packages/babel-plugin-react-forget/src/Optimization/ConstantPropagation.ts
+2
@@ -28,6 +28,7 @@ import {
28
} from "../HIR";
29
import {
30
removeDeadDoWhileStatements,
31
+ removeUnnecessaryTryCatch,
32
removeUnreachableForUpdates,
33
} from "../HIR/HIRBuilder";
34
import { eliminateRedundantPhi } from "../SSA";
@@ -66,6 +67,7 @@ function constantPropagationImpl(fn: HIRFunction, constants: Constants): void {
67
removeUnreachableFallthroughs(fn.body);
68
removeUnreachableForUpdates(fn.body);
69
removeDeadDoWhileStatements(fn.body);
70
+ removeUnnecessaryTryCatch(fn.body);
71
markInstructionIds(fn.body);
72
markPredecessors(fn.body);
73
compiler/packages/babel-plugin-react-forget/src/Optimization/PruneMaybeThrows.ts
+40
@@ -9,8 +9,20 @@ import {
9
GotoVariant,
10
HIRFunction,
11
Instruction,
12
+ assertConsistentIdentifiers,
13
+ assertTerminalSuccessorsExist,
14
mergeConsecutiveBlocks,
15
+ removeUnreachableFallthroughs,
16
+ reversePostorderBlocks,
17
} from "../HIR";
18
+import {
19
+ markInstructionIds,
20
+ markPredecessors,
21
+ removeDeadDoWhileStatements,
22
+ removeUnnecessaryTryCatch,
23
+ removeUnreachableForUpdates,
24
+} from "../HIR/HIRBuilder";
25
+import { eliminateRedundantPhi } from "../SSA";
26
27
/**
28
* This pass prunes `maybe-throw` terminals for blocks that can provably *never* throw.
@@ -20,7 +32,35 @@ import {
32
export function pruneMaybeThrows(fn: HIRFunction): void {
33
const didPrune = pruneMaybeThrowsImpl(fn);
34
if (didPrune) {
35
+ // If terminals have changed then blocks may have become newly unreachable.
36
+ // Re-run minification of the graph (incl reordering instruction ids)
37
+ reversePostorderBlocks(fn.body);
38
+ removeUnreachableFallthroughs(fn.body);
39
+ removeUnreachableForUpdates(fn.body);
40
+ removeDeadDoWhileStatements(fn.body);
41
+ removeUnnecessaryTryCatch(fn.body);
42
+ markInstructionIds(fn.body);
43
+ markPredecessors(fn.body);
44
+
45
+ // Now that predecessors are updated, prune phi operands that can never be reached
46
+ for (const [, block] of fn.body.blocks) {
47
+ for (const phi of block.phis) {
48
+ for (const [predecessor] of phi.operands) {
49
+ if (!block.preds.has(predecessor)) {
50
+ phi.operands.delete(predecessor);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ // By removing some phi operands, there may be phis that were not previously
56
+ // redundant but now are
57
+ eliminateRedundantPhi(fn);
58
+ // Finally, merge together any blocks that are now guaranteed to execute
59
+ // consecutively
60
mergeConsecutiveBlocks(fn);
61
+
62
+ assertConsistentIdentifiers(fn);
63
+ assertTerminalSuccessorsExist(fn);
64
}
65
}
66
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-empty-try.expect.md
new
+35
@@ -0,0 +1,35 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let x = props.default;
7
+ try {
8
+ } catch (e) {
9
+ x = e;
10
+ }
11
+ return x;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Component,
16
+ params: [{ default: 42 }],
17
+};
18
+
19
+```
20
+
21
+## Code
22
+
23
+```javascript
24
+function Component(props) {
25
+ const x = props.default;
26
+ return x;
27
+}
28
+
29
+export const FIXTURE_ENTRYPOINT = {
30
+ fn: Component,
31
+ params: [{ default: 42 }],
32
+};
33
+
34
+```
35
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-empty-try.js
new
+13
@@ -0,0 +1,13 @@
1
+function Component(props) {
2
+ let x = props.default;
3
+ try {
4
+ } catch (e) {
5
+ x = e;
6
+ }
7
+ return x;
8
+}
9
+
10
+export const FIXTURE_ENTRYPOINT = {
11
+ fn: Component,
12
+ params: [{ default: 42 }],
13
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-immediately-returns.expect.md
new
+38
@@ -0,0 +1,38 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let x = props.default;
7
+ try {
8
+ // note: has to be a primitive, we want an instruction that cannot throw
9
+ // to ensure there is no maybe-throw terminal
10
+ const y = 42;
11
+ return y;
12
+ } catch (e) {
13
+ x = e;
14
+ }
15
+ return x;
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: Component,
20
+ params: [{ default: 42 }],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+function Component(props) {
29
+ return 42;
30
+}
31
+
32
+export const FIXTURE_ENTRYPOINT = {
33
+ fn: Component,
34
+ params: [{ default: 42 }],
35
+};
36
+
37
+```
38
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-immediately-returns.js
new
+17
@@ -0,0 +1,17 @@
1
+function Component(props) {
2
+ let x = props.default;
3
+ try {
4
+ // note: has to be a primitive, we want an instruction that cannot throw
5
+ // to ensure there is no maybe-throw terminal
6
+ const y = 42;
7
+ return y;
8
+ } catch (e) {
9
+ x = e;
10
+ }
11
+ return x;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Component,
16
+ params: [{ default: 42 }],
17
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-immediately-throws-after-constant-propagation.expect.md
new
+38
@@ -0,0 +1,38 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ let x = props.default;
7
+ const y = 42;
8
+ try {
9
+ // note: this constant propagates so that we know
10
+ // the handler is unreachable
11
+ return y;
12
+ } catch (e) {
13
+ x = e;
14
+ }
15
+ return x;
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: Component,
20
+ params: [{ default: 42 }],
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+function Component(props) {
29
+ return 42;
30
+}
31
+
32
+export const FIXTURE_ENTRYPOINT = {
33
+ fn: Component,
34
+ params: [{ default: 42 }],
35
+};
36
+
37
+```
38
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/try-catch-try-immediately-throws-after-constant-propagation.js
new
+17
@@ -0,0 +1,17 @@
1
+function Component(props) {
2
+ let x = props.default;
3
+ const y = 42;
4
+ try {
5
+ // note: this constant propagates so that we know
6
+ // the handler is unreachable
7
+ return y;
8
+ } catch (e) {
9
+ x = e;
10
+ }
11
+ return x;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Component,
16
+ params: [{ default: 42 }],
17
+};