Simon Jakobi pushed to branch wip/sjakobi/T27368-cbe-compress at Glasgow Haskell Compiler / GHC
Commits:
-
948293c5
by Simon Jakobi at 2026-08-27T18:24:53+02:00
6 changed files:
- + changelog.d/27368
- compiler/GHC/Cmm/CommonBlockElim.hs
- compiler/GHC/Cmm/ContFlowOpt.hs
- compiler/GHC/Cmm/Pipeline.hs
- + testsuite/tests/codeGen/should_compile/T27368.hs
- testsuite/tests/codeGen/should_compile/all.T
Changes:
| 1 | +section: compiler
|
|
| 2 | +synopsis: Fix a ``setInfoTableStackMap`` panic caused by common block elimination
|
|
| 3 | + leaving references to eliminated blocks.
|
|
| 4 | +issues: #27368
|
|
| 5 | +mrs: !16543 |
| ... | ... | @@ -58,18 +58,57 @@ import qualified Data.List.NonEmpty as NE |
| 58 | 58 | -- hashes, and at most once otherwise. Previously, we were slower, and people
|
| 59 | 59 | -- rightfully complained: #10397
|
| 60 | 60 | |
| 61 | +-- Note that this pass both consumes and produces graphs that may
|
|
| 62 | +-- contain unreachable blocks: the control-flow optimiser that runs
|
|
| 63 | +-- before it can orphan blocks without deleting them, and such orphans
|
|
| 64 | +-- pass through this pass untouched. (The losing copy of each merge,
|
|
| 65 | +-- by contrast, is deleted here; see deleteLosers.) See
|
|
| 66 | +-- Note [unreachable blocks] in GHC.Cmm.Pipeline.
|
|
| 67 | + |
|
| 61 | 68 | -- TODO: Use optimization fuel
|
| 62 | 69 | elimCommonBlocks :: CmmGraph -> CmmGraph
|
| 63 | -elimCommonBlocks g = replaceLabels env $ copyTicks env g
|
|
| 70 | +elimCommonBlocks g = replaceLabels env $ deleteLosers $ copyTicks env g
|
|
| 64 | 71 | where
|
| 65 | - env = iterate mapEmpty blocks_with_key
|
|
| 66 | - -- The order of blocks doesn't matter here. While we could use
|
|
| 67 | - -- revPostorder which drops unreachable blocks this is done in
|
|
| 68 | - -- ContFlowOpt already which runs before this pass. So we use
|
|
| 69 | - -- toBlockList since it is faster.
|
|
| 72 | + -- Safe: replaceLabels is about to rewrite every in-graph reference
|
|
| 73 | + -- to a loser to its surviving chain end.
|
|
| 74 | + -- See Note [Resolving the CBE substitution].
|
|
| 75 | + deleteLosers g'
|
|
| 76 | + | mapNull env = g'
|
|
| 77 | + | otherwise = ofBlockMap (g_entry g')
|
|
| 78 | + (toBlockMap g' `mapDifference` env)
|
|
| 79 | + env = resolveSubst (iterate mapEmpty blocks_with_key)
|
|
| 80 | + -- The order of blocks doesn't matter here, so we use toBlockList,
|
|
| 81 | + -- which is faster than revPostorder.
|
|
| 70 | 82 | groups = groupByInt hash_block (toBlockList g) :: [[CmmBlock]]
|
| 71 | 83 | blocks_with_key = [ [ (successors b, [b]) | b <- bs] | bs <- groups]
|
| 72 | 84 | |
| 85 | +{- Note [Resolving the CBE substitution]
|
|
| 86 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 87 | +The substitution that `iterate` produces may contain chains
|
|
| 88 | +(k1 :-> k2, k2 :-> k3): the winner of one merge can lose a later one.
|
|
| 89 | +Its consumers, however, apply it non-transitively: replaceLabels
|
|
| 90 | +rewrites each label with a single map lookup, and copyTicks inverts
|
|
| 91 | +the substitution only one level deep. So before applying it we
|
|
| 92 | +resolve each entry to the end of its chain (resolveSubst, via
|
|
| 93 | +lookupBid, which does follow chains); the resolved substitution maps
|
|
| 94 | +every eliminated label directly to its final surviving
|
|
| 95 | +representative.
|
|
| 96 | + |
|
| 97 | +Applying the unresolved substitution instead would let replaceLabels
|
|
| 98 | +leave edges pointing at eliminated labels. Such edges can occur only
|
|
| 99 | +in blocks that themselves lost a merge, i.e. in unreachable code, but
|
|
| 100 | +even there they are harmful (#27368; analysed on the ticket). See
|
|
| 101 | +Note [unreachable blocks] in GHC.Cmm.Pipeline.
|
|
| 102 | +-}
|
|
| 103 | + |
|
| 104 | +-- | Resolve the substitution: follow chains (@k1 :-> k2@, @k2 :-> k3@)
|
|
| 105 | +-- to their ends, so that every eliminated label maps directly to its
|
|
| 106 | +-- final surviving representative.
|
|
| 107 | +--
|
|
| 108 | +-- See Note [Resolving the CBE substitution].
|
|
| 109 | +resolveSubst :: Subst -> Subst
|
|
| 110 | +resolveSubst env = mapMap (lookupBid env) env
|
|
| 111 | + |
|
| 73 | 112 | -- Invariant: The blocks in the list are pairwise distinct
|
| 74 | 113 | -- (so avoid comparing them again)
|
| 75 | 114 | type DistinctBlocks = [CmmBlock]
|
| ... | ... | @@ -44,7 +44,8 @@ import Control.Monad |
| 44 | 44 | --
|
| 45 | 45 | -- In this situation we say that we shortcut L2 to L3. One of
|
| 46 | 46 | -- consequences of shortcutting is that some blocks of code may become
|
| 47 | --- unreachable (in the example above this is true for L2).
|
|
| 47 | +-- unreachable (in the example above this is true for L2). See
|
|
| 48 | +-- Note [unreachable blocks] in GHC.Cmm.Pipeline.
|
|
| 48 | 49 | |
| 49 | 50 | |
| 50 | 51 | -- Note [Control-flow optimisations]
|
| ... | ... | @@ -66,7 +67,9 @@ import Control.Monad |
| 66 | 67 | --
|
| 67 | 68 | -- Blocks are processed using postorder DFS traversal. A side effect
|
| 68 | 69 | -- of determining traversal order with a graph search is elimination
|
| 69 | --- of any blocks that are unreachable.
|
|
| 70 | +-- of any blocks that are unreachable in the input; shortcutting can in
|
|
| 71 | +-- turn leave blocks unreachable in the output, see
|
|
| 72 | +-- Note [unreachable blocks] in GHC.Cmm.Pipeline.
|
|
| 70 | 73 | --
|
| 71 | 74 | -- Transformations are improved by working from the end of the graph
|
| 72 | 75 | -- towards the beginning, because we may be able to perform many
|
| ... | ... | @@ -341,8 +344,7 @@ blockConcat splitting_procs g@CmmGraph { g_entry = entry_id } |
| 341 | 344 | -- Invariant: if a block has no predecessors it should be dropped from the
|
| 342 | 345 | -- graph because it is unreachable. maybe_concat is constructed to maintain
|
| 343 | 346 | -- that invariant, but calling replaceLabels may introduce unreachable blocks.
|
| 344 | --- We rely on subsequent passes in the Cmm pipeline to remove unreachable
|
|
| 345 | --- blocks.
|
|
| 347 | +-- See Note [unreachable blocks] in GHC.Cmm.Pipeline.
|
|
| 346 | 348 | incPreds, decPreds :: BlockId -> LabelMap Int -> LabelMap Int
|
| 347 | 349 | incPreds bid edges = mapInsertWith (+) bid 1 edges
|
| 348 | 350 | decPreds bid edges = case mapLookup bid edges of
|
| ... | ... | @@ -358,6 +358,22 @@ containing junk code. These aren't necessarily a problem, but |
| 358 | 358 | removing them is good because it might save time in the native code
|
| 359 | 359 | generator later.
|
| 360 | 360 | |
| 361 | +Who removes them, and when? Any pass that rebuilds the graph from
|
|
| 362 | +revPostorder drops them as a side effect. Normally the first pass to
|
|
| 363 | +do so is stack layout. In addition, removeUnreachableBlocksProc at the
|
|
| 364 | +very end of cpsTop removes the blocks that the second round of
|
|
| 365 | +control-flow optimisation orphans, and prunes their info tables. No
|
|
| 366 | +pass between the first control-flow optimisation and stack layout
|
|
| 367 | +removes unreachable blocks, so every pass in that window may encounter
|
|
| 368 | +them.
|
|
| 369 | + |
|
| 370 | +Unreachable blocks are not entirely harmless, though. Later passes do
|
|
| 371 | +not uniformly restrict themselves to reachable code -- callProcPoints,
|
|
| 372 | +for example, folds over the whole block map. So every pass must keep
|
|
| 373 | +even unreachable code well-formed. Violating this caused the panic in
|
|
| 374 | +#27368 (analysed on the ticket). See
|
|
| 375 | +Note [Resolving the CBE substitution] in GHC.Cmm.CommonBlockElim.
|
|
| 376 | + |
|
| 361 | 377 | To make unreachable blocks visible in -ddump-cmm-* output, add -dppr-debug.
|
| 362 | 378 | -}
|
| 363 | 379 |
| 1 | +-- The two branches share an identical suffix from the inner case
|
|
| 2 | +-- onwards, so common block elimination merges the duplicated call
|
|
| 3 | +-- blocks over several rounds, building a substitution chain. Without
|
|
| 4 | +-- resolving that chain, compiling this module at -O2 panicked in
|
|
| 5 | +-- setInfoTableStackMap (#27368). See
|
|
| 6 | +-- Note [Resolving the CBE substitution] in GHC.Cmm.CommonBlockElim.
|
|
| 7 | + |
|
| 8 | +module T27368 (f) where
|
|
| 9 | + |
|
| 10 | +{-# NOINLINE put #-}
|
|
| 11 | +put :: Int -> Int -> IO ()
|
|
| 12 | +put h x = if h + x == 12345 then errorWithoutStackTrace "boom" else pure ()
|
|
| 13 | + |
|
| 14 | +data T = J Int | K
|
|
| 15 | + |
|
| 16 | +f :: Int -> Bool -> T -> IO ()
|
|
| 17 | +f h a t = do
|
|
| 18 | + if a
|
|
| 19 | + then do put h 1; case t of { J _ -> put h 3; K -> put h 4 }; put h 0; put h 0
|
|
| 20 | + else do put h 2; case t of { J _ -> put h 3; K -> put h 4 }; put h 0; put h 0
|
|
| 21 | + put h 0 |
| ... | ... | @@ -150,3 +150,5 @@ test('T16351', normal, compile, ['-O2 -ddump-simpl -dno-typeable-binds -dsuppres |
| 150 | 150 | test('T20298a', normal, compile, ['-O2 -ddump-simpl -dno-typeable-binds -dsuppress-all -dsuppress-uniques'])
|
| 151 | 151 | test('T20298b', normal, compile, ['-O2 -dno-bignum-rules -ddump-simpl -dno-typeable-binds -dsuppress-all -dsuppress-uniques'])
|
| 152 | 152 | test('T20298c', normal, compile, ['-O2 -dno-builtin-rules -ddump-simpl -dno-typeable-binds -dsuppress-all -dsuppress-uniques'])
|
| 153 | + |
|
| 154 | +test('T27368', normal, compile, ['-O2']) |