@samitouri / QOS-React / commits / 2ddbbd4735

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