Exhaustive switches for some terminal handling
Updates two points in the compiler that were easy to miss when adding new terminals: * HIRBuilder's `removeUnreachableFallthroughs()` nulls out unreachable fallthroughs, but this had a non-exhaustive `if` statement. It now uses a helper function which internally has an exhaustive switch. * LeaveSSA needs to schedule block fallthroughs, but had a non-exhaustive `if` statement. It also uses a helper function which internally has an exhaustive switch. cc @poteto since you ran into this (ie the compiler not alerting you to update these places) w your diffs.
Joe Savona committed
Apr 26, 2023 at 16:34 UTC
2ddbbd4735df509a5e34ad2ec2bcbc6beacb88db
3 files changed
+121
-32
compiler/forget/src/HIR/HIRBuilder.ts
+11
-13
@@ -28,7 +28,11 @@ import {
28
Terminal,
29
} from "./HIR";
30
import { printInstruction } from "./PrintHIR";
31
-import { eachTerminalSuccessor, mapTerminalSuccessors } from "./visitors";
31
+import {
32
+ eachTerminalSuccessor,
33
+ mapOptionalFallthroughs,
34
+ mapTerminalSuccessors,
35
+} from "./visitors";
36
37
// *******************************************************************************************
38
// *******************************************************************************************
@@ -535,19 +539,13 @@ export function removeUnreachableFallthroughs(func: HIR): void {
539
540
// Cleanup any fallthrough blocks that weren't visited
541
for (const [_, block] of func.blocks) {
538
- if (
539
- block.terminal.kind === "if" ||
540
- block.terminal.kind === "switch" ||
541
- block.terminal.kind === "while" ||
542
- block.terminal.kind === "label"
543
- ) {
544
- if (
545
- block.terminal.fallthrough !== null &&
546
- !visited.has(block.terminal.fallthrough)
547
- ) {
548
- block.terminal.fallthrough = null;
542
+ mapOptionalFallthroughs(block.terminal, (fallthrough) => {
543
+ if (visited.has(fallthrough)) {
544
+ return fallthrough;
545
+ } else {
546
+ return null;
547
}
550
- }
548
+ });
549
}
550
}
551
compiler/forget/src/HIR/visitors.ts
+106
@@ -701,6 +701,112 @@ export function mapTerminalSuccessors(
701
}
702
}
703
704
+/**
705
+ * Helper to get a terminal's fallthrough. The main reason to extract this as a helper
706
+ * function is to ensure that we use an exhaustive switch to ensure that we add new terminal
707
+ * variants as appropriate.
708
+ */
709
+export function terminalFallthrough(terminal: Terminal): BlockId | null {
710
+ switch (terminal.kind) {
711
+ case "branch":
712
+ case "goto":
713
+ case "return":
714
+ case "throw":
715
+ case "unsupported": {
716
+ return null;
717
+ }
718
+ case "do-while":
719
+ case "for-of":
720
+ case "for":
721
+ case "if":
722
+ case "label":
723
+ case "logical":
724
+ case "optional-call":
725
+ case "switch":
726
+ case "ternary":
727
+ case "while": {
728
+ return terminal.fallthrough;
729
+ }
730
+ default: {
731
+ assertExhaustive(
732
+ terminal,
733
+ `Unexpected terminal kind '${(terminal as any).kind}'`
734
+ );
735
+ }
736
+ }
737
+}
738
+
739
+export function mapOptionalFallthroughs(
740
+ terminal: Terminal,
741
+ fn: (block: BlockId) => BlockId | null
742
+): void {
743
+ switch (terminal.kind) {
744
+ case "branch":
745
+ case "goto":
746
+ case "return":
747
+ case "throw":
748
+ case "unsupported": {
749
+ return;
750
+ }
751
+ // NOTE: TypeScript has a bug where it does not correctly model properties whose values are
752
+ // non-null in some cases and nullable in other cases, if those cases are joined together.
753
+ // Thus we use one block per case here to ensure that any changes to the types will cause
754
+ // a compiler error.
755
+ case "do-while": {
756
+ const _: BlockId = terminal.fallthrough;
757
+ break;
758
+ }
759
+ case "for-of": {
760
+ const _: BlockId = terminal.fallthrough;
761
+ break;
762
+ }
763
+ case "for": {
764
+ const _: BlockId = terminal.fallthrough;
765
+ break;
766
+ }
767
+ case "logical": {
768
+ const _: BlockId = terminal.fallthrough;
769
+ break;
770
+ }
771
+ case "optional-call": {
772
+ const _: BlockId = terminal.fallthrough;
773
+ break;
774
+ }
775
+ case "ternary": {
776
+ const _: BlockId = terminal.fallthrough;
777
+ break;
778
+ }
779
+ case "while": {
780
+ const _: BlockId = terminal.fallthrough;
781
+ break;
782
+ }
783
+ case "switch": {
784
+ if (terminal.fallthrough !== null) {
785
+ terminal.fallthrough = fn(terminal.fallthrough);
786
+ }
787
+ break;
788
+ }
789
+ case "if": {
790
+ if (terminal.fallthrough !== null) {
791
+ terminal.fallthrough = fn(terminal.fallthrough);
792
+ }
793
+ break;
794
+ }
795
+ case "label": {
796
+ if (terminal.fallthrough !== null) {
797
+ terminal.fallthrough = fn(terminal.fallthrough);
798
+ }
799
+ break;
800
+ }
801
+ default: {
802
+ assertExhaustive(
803
+ terminal,
804
+ `Unexpected terminal kind '${(terminal as any).kind}'`
805
+ );
806
+ }
807
+ }
808
+}
809
+
810
/**
811
* Iterates over the successor block ids of the provided terminal. The function is called
812
* specifically for the successors that define the standard control flow, and not
compiler/forget/src/SSA/LeaveSSA.ts
+4
-19
@@ -28,6 +28,7 @@ import {
28
eachInstructionValueOperand,
29
eachPatternOperand,
30
eachTerminalOperand,
31
+ terminalFallthrough,
32
} from "../HIR/visitors";
33
34
/**
@@ -294,17 +295,9 @@ export function leaveSSA(fn: HIRFunction): void {
295
}
296
}
297
}
297
- if (
298
- (terminal.kind === "if" ||
299
- terminal.kind === "switch" ||
300
- terminal.kind === "while" ||
301
- terminal.kind === "do-while" ||
302
- terminal.kind === "for" ||
303
- terminal.kind === "for-of" ||
304
- terminal.kind === "label") &&
305
- terminal.fallthrough !== null
306
- ) {
307
- const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
298
+ const fallthroughId = terminalFallthrough(terminal);
299
+ if (fallthroughId !== null) {
300
+ const fallthrough = fn.body.blocks.get(fallthroughId)!;
301
pushPhis(fallthrough);
302
}
303
if (terminal.kind === "while" || terminal.kind === "for") {
@@ -345,14 +338,6 @@ export function leaveSSA(fn: HIRFunction): void {
338
pushPhis(update);
339
}
340
}
348
- if (
349
- terminal.kind === "logical" ||
350
- terminal.kind === "ternary" ||
351
- terminal.kind === "optional-call"
352
- ) {
353
- const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
354
- pushPhis(fallthrough);
355
- }
341
342
for (const { phi, block: phiBlock } of reassignmentPhis) {
343
// In some cases one of the phi operands can be defined *before* the let binding