@samitouri / QOS-React / commits / de2b684bc5

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