From 0fe2b410ac0d8951f07ffcc9f3c6c97bc312df48 Mon Sep 17 00:00:00 2001
From: Andreas Klebinger <klebinger.andreas@gmx.at>
Date: Fri, 5 Apr 2024 14:53:39 +0200
Subject: [PATCH] NCG: Fix a bug where we errounously removed a required jump
 instruction.

Add a new method to the Instruction class to check if we can eliminate a
jump in favour of fallthrough control flow.

Fixes #24507
---
 compiler/GHC/CmmToAsm/AArch64.hs              |  1 +
 compiler/GHC/CmmToAsm/AArch64/Instr.hs        |  6 ++++
 compiler/GHC/CmmToAsm/BlockLayout.hs          |  5 ++-
 compiler/GHC/CmmToAsm/Instr.hs                |  8 ++++-
 compiler/GHC/CmmToAsm/Monad.hs                | 26 ++++++++++++++
 compiler/GHC/CmmToAsm/PPC.hs                  |  1 +
 compiler/GHC/CmmToAsm/PPC/Instr.hs            |  8 +++++
 compiler/GHC/CmmToAsm/Reg/Liveness.hs         |  5 +++
 compiler/GHC/CmmToAsm/X86.hs                  |  1 +
 compiler/GHC/CmmToAsm/X86/Instr.hs            | 12 +++++++
 testsuite/tests/codeGen/should_run/T24507.hs  | 15 ++++++++
 .../tests/codeGen/should_run/T24507.stdout    |  7 ++++
 .../tests/codeGen/should_run/T24507_cmm.cmm   | 35 +++++++++++++++++++
 13 files changed, 126 insertions(+), 4 deletions(-)
 create mode 100644 testsuite/tests/codeGen/should_run/T24507.hs
 create mode 100644 testsuite/tests/codeGen/should_run/T24507.stdout
 create mode 100644 testsuite/tests/codeGen/should_run/T24507_cmm.cmm

diff --git a/compiler/GHC/CmmToAsm/AArch64.hs b/compiler/GHC/CmmToAsm/AArch64.hs
index 1977aeb1a7ec..66b6ba1f60a4 100644
--- a/compiler/GHC/CmmToAsm/AArch64.hs
+++ b/compiler/GHC/CmmToAsm/AArch64.hs
@@ -47,6 +47,7 @@ instance Instruction AArch64.Instr where
         patchRegsOfInstr        = AArch64.patchRegsOfInstr
         isJumpishInstr          = AArch64.isJumpishInstr
         jumpDestsOfInstr        = AArch64.jumpDestsOfInstr
+        canFallthroughTo        = AArch64.canFallthroughTo
         patchJumpInstr          = AArch64.patchJumpInstr
         mkSpillInstr            = AArch64.mkSpillInstr
         mkLoadInstr             = AArch64.mkLoadInstr
diff --git a/compiler/GHC/CmmToAsm/AArch64/Instr.hs b/compiler/GHC/CmmToAsm/AArch64/Instr.hs
index 2fd595d76e47..6d4dad4f774b 100644
--- a/compiler/GHC/CmmToAsm/AArch64/Instr.hs
+++ b/compiler/GHC/CmmToAsm/AArch64/Instr.hs
@@ -317,6 +317,12 @@ jumpDestsOfInstr (BL t _ _) = [ id | TBlock id <- [t]]
 jumpDestsOfInstr (BCOND _ t) = [ id | TBlock id <- [t]]
 jumpDestsOfInstr _ = []
 
+canFallthroughTo :: Instr -> BlockId -> Bool
+canFallthroughTo (ANN _ i) bid = canFallthroughTo i bid
+canFallthroughTo (J (TBlock target)) bid = bid == target
+canFallthroughTo (B (TBlock target)) bid = bid == target
+canFallthroughTo _ _ = False
+
 -- | Change the destination of this jump instruction.
 -- Used in the linear allocator when adding fixup blocks for join
 -- points.
diff --git a/compiler/GHC/CmmToAsm/BlockLayout.hs b/compiler/GHC/CmmToAsm/BlockLayout.hs
index 052d4ae3c288..6488c3c24935 100644
--- a/compiler/GHC/CmmToAsm/BlockLayout.hs
+++ b/compiler/GHC/CmmToAsm/BlockLayout.hs
@@ -771,10 +771,9 @@ dropJumps :: forall a i. Instruction i => LabelMap a -> [GenBasicBlock i]
 dropJumps _    [] = []
 dropJumps info (BasicBlock lbl ins:todo)
     | Just ins <- nonEmpty ins --This can happen because of shortcutting
-    , [dest] <- jumpDestsOfInstr (NE.last ins)
     , BasicBlock nextLbl _ : _ <- todo
-    , not (mapMember dest info)
-    , nextLbl == dest
+    , canFallthroughTo (NE.last ins) nextLbl
+    , not (mapMember nextLbl info)
     = BasicBlock lbl (NE.init ins) : dropJumps info todo
     | otherwise
     = BasicBlock lbl ins : dropJumps info todo
diff --git a/compiler/GHC/CmmToAsm/Instr.hs b/compiler/GHC/CmmToAsm/Instr.hs
index aa8f538e07ee..448173a68254 100644
--- a/compiler/GHC/CmmToAsm/Instr.hs
+++ b/compiler/GHC/CmmToAsm/Instr.hs
@@ -71,11 +71,17 @@ class Instruction instr where
                 :: instr -> Bool
 
 
-        -- | Give the possible destinations of this jump instruction.
+        -- | Give the possible *local block* destinations of this jump instruction.
         --      Must be defined for all jumpish instructions.
         jumpDestsOfInstr
                 :: instr -> [BlockId]
 
+        -- | Check if the instr always transfers control flow
+        -- to the given block. Used by code layout to eliminate
+        -- jumps that can be replaced by fall through.
+        canFallthroughTo
+                :: instr -> BlockId -> Bool
+
 
         -- | Change the destination of this jump instruction.
         --      Used in the linear allocator when adding fixup blocks for join
diff --git a/compiler/GHC/CmmToAsm/Monad.hs b/compiler/GHC/CmmToAsm/Monad.hs
index 38bbca79fcce..2499259b1e42 100644
--- a/compiler/GHC/CmmToAsm/Monad.hs
+++ b/compiler/GHC/CmmToAsm/Monad.hs
@@ -78,8 +78,15 @@ data NcgImpl statics instr jumpDest = NcgImpl {
     cmmTopCodeGen             :: RawCmmDecl -> NatM [NatCmmDecl statics instr],
     generateJumpTableForInstr :: instr -> Maybe (NatCmmDecl statics instr),
     getJumpDestBlockId        :: jumpDest -> Maybe BlockId,
+    -- | Does this jump always jump to a single destination and is shortcutable?
+    --
+    -- We use this to determine shortcutable instructions - See Note [What is shortcutting]
+    -- Note that if we return a destination here we *most* support the relevant shortcutting in
+    -- shortcutStatics for jump tables and shortcutJump for the instructions itself.
     canShortcut               :: instr -> Maybe jumpDest,
+    -- | Replace references to blockIds with other destinations - used to update jump tables.
     shortcutStatics           :: (BlockId -> Maybe jumpDest) -> statics -> statics,
+    -- | Change the jump destination(s) of an instruction.
     shortcutJump              :: (BlockId -> Maybe jumpDest) -> instr -> instr,
     -- | 'Module' is only for printing internal labels. See Note [Internal proc
     -- labels] in CLabel.
@@ -105,6 +112,25 @@ data NcgImpl statics instr jumpDest = NcgImpl {
     -- when possible.
     }
 
+{- Note [supporting shortcutting]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For the concept of shortcutting see Note [What is shortcutting].
+
+In order to support shortcutting across multiple backends uniformly we
+use canShortcut, shortcutStatics and shortcutJump.
+
+canShortcut tells us if the backend support shortcutting of a instruction
+and if so what destination we should retarget instruction to instead.
+
+shortcutStatics exists to allow us to update jump destinations in jump tables.
+
+shortcutJump updates the instructions itself.
+
+A backend can opt out of those by always returning Nothing for canShortcut
+and implementing shortcutStatics/shortcutJump as \_ x -> x
+
+-}
+
 {- Note [pprNatCmmDeclS and pprNatCmmDeclH]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Each NcgImpl provides two implementations of its CmmDecl printer, pprNatCmmDeclS
diff --git a/compiler/GHC/CmmToAsm/PPC.hs b/compiler/GHC/CmmToAsm/PPC.hs
index 40a629907ffb..60e5767a45e0 100644
--- a/compiler/GHC/CmmToAsm/PPC.hs
+++ b/compiler/GHC/CmmToAsm/PPC.hs
@@ -46,6 +46,7 @@ instance Instruction PPC.Instr where
    patchRegsOfInstr    = PPC.patchRegsOfInstr
    isJumpishInstr      = PPC.isJumpishInstr
    jumpDestsOfInstr    = PPC.jumpDestsOfInstr
+   canFallthroughTo    = PPC.canFallthroughTo
    patchJumpInstr      = PPC.patchJumpInstr
    mkSpillInstr        = PPC.mkSpillInstr
    mkLoadInstr         = PPC.mkLoadInstr
diff --git a/compiler/GHC/CmmToAsm/PPC/Instr.hs b/compiler/GHC/CmmToAsm/PPC/Instr.hs
index c0783ae29368..a1ee6f8dae7f 100644
--- a/compiler/GHC/CmmToAsm/PPC/Instr.hs
+++ b/compiler/GHC/CmmToAsm/PPC/Instr.hs
@@ -22,6 +22,7 @@ module GHC.CmmToAsm.PPC.Instr
    , patchJumpInstr
    , patchRegsOfInstr
    , jumpDestsOfInstr
+   , canFallthroughTo
    , takeRegRegMoveInstr
    , takeDeltaInstr
    , mkRegRegMoveInstr
@@ -509,6 +510,13 @@ isJumpishInstr instr
     JMP{}       -> True
     _           -> False
 
+canFallthroughTo :: Instr -> BlockId -> Bool
+canFallthroughTo instr bid
+ = case instr of
+        BCC _ target _      -> target == bid
+        BCCFAR _ target _   -> target == bid
+        _                   -> False
+
 
 -- | Checks whether this instruction is a jump/branch instruction.
 -- One that can change the flow of control in a way that the
diff --git a/compiler/GHC/CmmToAsm/Reg/Liveness.hs b/compiler/GHC/CmmToAsm/Reg/Liveness.hs
index 0a0aa247a41f..bb1bc0b451c2 100644
--- a/compiler/GHC/CmmToAsm/Reg/Liveness.hs
+++ b/compiler/GHC/CmmToAsm/Reg/Liveness.hs
@@ -126,6 +126,11 @@ instance Instruction instr => Instruction (InstrSR instr) where
                 Instr instr     -> isJumpishInstr instr
                 _               -> False
 
+        canFallthroughTo i bid
+         = case i of
+                Instr instr     -> canFallthroughTo instr bid
+                _               -> False
+
         jumpDestsOfInstr i
          = case i of
                 Instr instr     -> jumpDestsOfInstr instr
diff --git a/compiler/GHC/CmmToAsm/X86.hs b/compiler/GHC/CmmToAsm/X86.hs
index a13fa2e4656d..835edee7aa0e 100644
--- a/compiler/GHC/CmmToAsm/X86.hs
+++ b/compiler/GHC/CmmToAsm/X86.hs
@@ -51,6 +51,7 @@ instance Instruction X86.Instr where
    patchRegsOfInstr        = X86.patchRegsOfInstr
    isJumpishInstr          = X86.isJumpishInstr
    jumpDestsOfInstr        = X86.jumpDestsOfInstr
+   canFallthroughTo        = X86.canFallthroughTo
    patchJumpInstr          = X86.patchJumpInstr
    mkSpillInstr            = X86.mkSpillInstr
    mkLoadInstr             = X86.mkLoadInstr
diff --git a/compiler/GHC/CmmToAsm/X86/Instr.hs b/compiler/GHC/CmmToAsm/X86/Instr.hs
index 8cdf26f40e0f..679849123ecc 100644
--- a/compiler/GHC/CmmToAsm/X86/Instr.hs
+++ b/compiler/GHC/CmmToAsm/X86/Instr.hs
@@ -31,6 +31,7 @@ module GHC.CmmToAsm.X86.Instr
    , mkSpillInstr
    , mkRegRegMoveInstr
    , jumpDestsOfInstr
+   , canFallthroughTo
    , patchRegsOfInstr
    , patchJumpInstr
    , isMetaInstr
@@ -669,6 +670,17 @@ isJumpishInstr instr
         CALL{}          -> True
         _               -> False
 
+canFallthroughTo :: Instr -> BlockId -> Bool
+canFallthroughTo insn bid
+  = case insn of
+    JXX _ target          -> bid == target
+    JMP_TBL _ targets _ _ -> all isTargetBid targets
+    _                     -> False
+  where
+    isTargetBid target = case target of
+      Nothing                      -> True
+      Just (DestBlockId target) -> target == bid
+      _                  -> False
 
 jumpDestsOfInstr
         :: Instr
diff --git a/testsuite/tests/codeGen/should_run/T24507.hs b/testsuite/tests/codeGen/should_run/T24507.hs
new file mode 100644
index 000000000000..31fd42a52c8b
--- /dev/null
+++ b/testsuite/tests/codeGen/should_run/T24507.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE MagicHash #-}
+{-# LANGUAGE UnboxedTuples #-}
+{-# LANGUAGE GHCForeignImportPrim #-}
+{-# LANGUAGE UnliftedFFITypes #-}
+
+module Main where
+
+import GHC.Exts
+
+foreign import prim "foo" foo :: Int# -> Int#
+
+main = do
+
+    let f x = case x of I# x' -> case foo x' of x -> print (I# x)
+    mapM_ f [1..7]
\ No newline at end of file
diff --git a/testsuite/tests/codeGen/should_run/T24507.stdout b/testsuite/tests/codeGen/should_run/T24507.stdout
new file mode 100644
index 000000000000..f4b0843eecbe
--- /dev/null
+++ b/testsuite/tests/codeGen/should_run/T24507.stdout
@@ -0,0 +1,7 @@
+1
+2
+2
+2
+2
+2
+2
diff --git a/testsuite/tests/codeGen/should_run/T24507_cmm.cmm b/testsuite/tests/codeGen/should_run/T24507_cmm.cmm
new file mode 100644
index 000000000000..28db7bf7f674
--- /dev/null
+++ b/testsuite/tests/codeGen/should_run/T24507_cmm.cmm
@@ -0,0 +1,35 @@
+#include "Cmm.h"
+
+bar() {
+    return (2);
+}
+
+foo(W_ x) {
+
+    switch(x) {
+        case 1: goto a;
+        case 2: goto b;
+        case 3: goto c;
+        case 4: goto d;
+        case 5: goto e;
+        case 6: goto f;
+        case 7: goto g;
+    }
+    return (1);
+
+    a:
+    return (1);
+    b:
+    jump bar();
+    c:
+    jump bar();
+    d:
+    jump bar();
+    e:
+    jump bar();
+    f:
+    jump bar();
+    g:
+    jump bar();
+
+}
-- 
GitLab