Simon Jakobi pushed to branch wip/sjakobi/T27296-stable-simpl at Glasgow Haskell Compiler / GHC
Commits:
-
69b3d03a
by Simon Jakobi at 2026-06-05T13:56:09+02:00
12 changed files:
- + changelog.d/stable-core-dump-order-27296
- compiler/GHC/Core/Lint.hs
- compiler/GHC/Core/Ppr.hs
- compiler/GHC/Driver/DynFlags.hs
- compiler/GHC/Driver/Flags.hs
- compiler/GHC/Driver/Session.hs
- compiler/GHC/Utils/Outputable.hs
- docs/users_guide/debugging.rst
- testsuite/tests/simplCore/should_compile/Makefile
- + testsuite/tests/simplCore/should_compile/T27296.hs
- + testsuite/tests/simplCore/should_compile/T27296.stdout
- testsuite/tests/simplCore/should_compile/all.T
Changes:
| 1 | +section: compiler
|
|
| 2 | +synopsis: Add :ghc-flag:`-dstable-core-dump-order` to print top-level Core bindings in a stable, source-location-based order that does not depend on uniques, making Core dumps easier to diff.
|
|
| 3 | +issues: #27296
|
|
| 4 | +mrs: !16143 |
| ... | ... | @@ -327,12 +327,17 @@ dumpPassResult logger dump_core_sizes name_ppr_ctx mb_flag hdr extra_info binds |
| 327 | 327 | where
|
| 328 | 328 | size_doc = sep [text "Result size of" <+> text hdr, nest 2 (equals <+> ppr (coreBindsStats binds))]
|
| 329 | 329 | |
| 330 | + -- See Note [Stable Core dump order] in GHC.Core.Ppr
|
|
| 331 | + binds' | sdocStableCoreDumpOrder (log_default_dump_context (logFlags logger))
|
|
| 332 | + = sortCoreBindingsForDump binds
|
|
| 333 | + | otherwise = binds
|
|
| 334 | + |
|
| 330 | 335 | dump_doc = vcat [ nest 2 extra_info
|
| 331 | 336 | , size_doc
|
| 332 | 337 | , blankLine
|
| 333 | 338 | , if dump_core_sizes
|
| 334 | - then pprCoreBindingsWithSize binds
|
|
| 335 | - else pprCoreBindings binds
|
|
| 339 | + then pprCoreBindingsWithSize binds'
|
|
| 340 | + else pprCoreBindings binds'
|
|
| 336 | 341 | , ppUnless (null rules) pp_rules ]
|
| 337 | 342 | pp_rules = vcat [ blankLine
|
| 338 | 343 | , text "------ Local rules for imported ids --------"
|
| ... | ... | @@ -19,6 +19,7 @@ module GHC.Core.Ppr ( |
| 19 | 19 | pprCoreExpr, pprParendExpr,
|
| 20 | 20 | pprCoreBinding, pprCoreBindings, pprCoreAlt,
|
| 21 | 21 | pprCoreBindingWithSize, pprCoreBindingsWithSize,
|
| 22 | + sortCoreBindingsForDump,
|
|
| 22 | 23 | pprCoreBinder, pprCoreBinders, pprId, pprIds,
|
| 23 | 24 | pprRule, pprRules, pprOptCo,
|
| 24 | 25 | pprOcc, pprOccWithTick
|
| ... | ... | @@ -30,7 +31,7 @@ import GHC.Core |
| 30 | 31 | import GHC.Core.Stats (exprStats)
|
| 31 | 32 | import GHC.Types.Fixity (LexicalFixity(..))
|
| 32 | 33 | import GHC.Types.Literal( pprLiteral )
|
| 33 | -import GHC.Types.Name( pprInfixName, pprPrefixName )
|
|
| 34 | +import GHC.Types.Name( pprInfixName, pprPrefixName, getOccString, getSrcSpan )
|
|
| 34 | 35 | import GHC.Types.Var
|
| 35 | 36 | import GHC.Types.Id
|
| 36 | 37 | import GHC.Types.Id.Info
|
| ... | ... | @@ -44,9 +45,13 @@ import GHC.Core.Coercion |
| 44 | 45 | import GHC.Types.Basic
|
| 45 | 46 | import GHC.Utils.Misc
|
| 46 | 47 | import GHC.Utils.Outputable
|
| 47 | -import GHC.Types.SrcLoc ( pprUserRealSpan )
|
|
| 48 | +import GHC.Utils.Panic (panic)
|
|
| 49 | +import GHC.Types.SrcLoc ( SrcSpan(..), srcSpanStartLine, srcSpanStartCol
|
|
| 50 | + , pprUserRealSpan )
|
|
| 48 | 51 | import GHC.Types.Tickish
|
| 49 | 52 | |
| 53 | +import Data.List ( sortOn )
|
|
| 54 | + |
|
| 50 | 55 | {-
|
| 51 | 56 | ************************************************************************
|
| 52 | 57 | * *
|
| ... | ... | @@ -71,6 +76,70 @@ pprCoreBindingWithSize :: CoreBind -> SDoc |
| 71 | 76 | pprCoreBindingsWithSize = pprTopBinds sizeAnn
|
| 72 | 77 | pprCoreBindingWithSize = pprTopBind sizeAnn
|
| 73 | 78 | |
| 79 | +{- Note [Stable Core dump order]
|
|
| 80 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
| 81 | +The order of top-level bindings in a Core dump (-ddump-simpl etc.) is the
|
|
| 82 | +compiler's internal processing order, which is sensitive to Uniques. Uniques
|
|
| 83 | +can shift whenever an unrelated upstream module changes, so the bindings get
|
|
| 84 | +re-ordered and a textual diff of two dumps fails to line up the real changes
|
|
| 85 | +(#27296).
|
|
| 86 | + |
|
| 87 | +With -dstable-core-dump-order we reorder the top-level bindings at dump time into
|
|
| 88 | +a stable, diffable order. 'sortCoreBindingsForDump' sorts by a key that is
|
|
| 89 | +*independent of Uniques* (so the output is diffable even without
|
|
| 90 | +-dsuppress-uniques):
|
|
| 91 | + |
|
| 92 | + 1. the binder's source span (real spans in source order; noSrcSpan last).
|
|
| 93 | + Workers and specialisations inherit their origin's source span (see
|
|
| 94 | + 'mkWorkerId' and 'newSpecIdSM'), so they cluster next to the binding they
|
|
| 95 | + come from.
|
|
| 96 | + 2. a "$-rank" so that within one source span the compiler-derived binders sort
|
|
| 97 | + *before* the origin they come from (e.g. @$wfoo@ before @foo@), mirroring
|
|
| 98 | + GHC's default dependency order (the wrapper calls the worker, so the worker
|
|
| 99 | + comes first; specialisations likewise precede their origin). We rank by
|
|
| 100 | + whether the OccName *contains* a '$', which marks a derived binder: a worker
|
|
| 101 | + is @$wfoo@, but a call-site specialisation is tidied to @bar_$sfoo@ (no
|
|
| 102 | + leading '$'), so a leading-'$' test would miss it.
|
|
| 103 | + 3. the OccName string, as a final lexical, deterministic tie-break.
|
|
| 104 | + |
|
| 105 | +Recursive groups are never split: a 'Rec' is one 'CoreBind', placed as a unit by
|
|
| 106 | +its earliest-source member, with its members sorted by the same key.
|
|
| 107 | + |
|
| 108 | +-dstable-core-dump-order is opt-in; the default order is retained because it is
|
|
| 109 | +useful for debugging the compiler itself.
|
|
| 110 | +-}
|
|
| 111 | + |
|
| 112 | +-- | Reorder a 'CoreProgram' into a stable, source-location-driven order for
|
|
| 113 | +-- dumping. See Note [Stable Core dump order]. Used by 'dumpPassResult' when
|
|
| 114 | +-- -dstable-core-dump-order is enabled.
|
|
| 115 | +sortCoreBindingsForDump :: CoreProgram -> CoreProgram
|
|
| 116 | +sortCoreBindingsForDump = sortOn bindKey . map sortRecMembers
|
|
| 117 | + where
|
|
| 118 | + sortRecMembers (Rec prs) = Rec (sortOn (bndrKey . fst) prs)
|
|
| 119 | + sortRecMembers b = b
|
|
| 120 | + |
|
| 121 | + -- 'sortRecMembers' runs first, so a 'Rec' is already sorted by 'bndrKey'
|
|
| 122 | + -- when 'bindKey' sees it; its first member is therefore the minimum key.
|
|
| 123 | + bindKey :: CoreBind -> (Int, Int, Int, Int, String)
|
|
| 124 | + bindKey (NonRec b _) = bndrKey b
|
|
| 125 | + bindKey (Rec ((b,_):_)) = bndrKey b
|
|
| 126 | + bindKey (Rec []) = panic "sortCoreBindingsForDump: empty Rec"
|
|
| 127 | + |
|
| 128 | + bndrKey :: CoreBndr -> (Int, Int, Int, Int, String)
|
|
| 129 | + bndrKey b = (bucket, line, col, dollar_rank, s)
|
|
| 130 | + where
|
|
| 131 | + s = getOccString b
|
|
| 132 | + (bucket, line, col) = case getSrcSpan b of
|
|
| 133 | + RealSrcSpan rs _ -> (0, srcSpanStartLine rs, srcSpanStartCol rs)
|
|
| 134 | + _ -> (1, 0, 0) -- noSrcSpan: sort last
|
|
| 135 | + -- A '$' anywhere in a tidied top-level OccName marks a compiler-derived
|
|
| 136 | + -- binder ($wfoo, but also call-site specialisations tidied to
|
|
| 137 | + -- bar_$sfoo); rank those before their origin within a shared source span,
|
|
| 138 | + -- mirroring GHC's default dependency order (the wrapper calls the worker,
|
|
| 139 | + -- so the worker comes first).
|
|
| 140 | + dollar_rank | '$' `elem` s = 0
|
|
| 141 | + | otherwise = 1
|
|
| 142 | + |
|
| 74 | 143 | instance OutputableBndr b => Outputable (Bind b) where
|
| 75 | 144 | ppr bind = ppr_bind noAnn bind
|
| 76 | 145 |
| ... | ... | @@ -1594,6 +1594,7 @@ initSDocContext dflags style = SDC |
| 1594 | 1594 | , sdocSuppressModulePrefixes = gopt Opt_SuppressModulePrefixes dflags
|
| 1595 | 1595 | , sdocSuppressStgExts = gopt Opt_SuppressStgExts dflags
|
| 1596 | 1596 | , sdocSuppressStgReps = gopt Opt_SuppressStgReps dflags
|
| 1597 | + , sdocStableCoreDumpOrder = gopt Opt_StableCoreDumpOrder dflags
|
|
| 1597 | 1598 | , sdocErrorSpans = gopt Opt_ErrorSpans dflags
|
| 1598 | 1599 | , sdocStarIsType = xopt LangExt.StarIsType dflags
|
| 1599 | 1600 | , sdocLinearTypes = xopt LangExt.LinearTypes dflags
|
| ... | ... | @@ -859,6 +859,10 @@ data GeneralFlag |
| 859 | 859 | | Opt_SuppressTimestamps -- ^ Suppress timestamps in dumps
|
| 860 | 860 | | Opt_SuppressCoreSizes -- ^ Suppress per binding Core size stats in dumps
|
| 861 | 861 | |
| 862 | + -- | Reorder top-level bindings in Core dumps into a stable, diffable order.
|
|
| 863 | + -- See Note [Stable Core dump order] in GHC.Core.Ppr.
|
|
| 864 | + | Opt_StableCoreDumpOrder
|
|
| 865 | + |
|
| 862 | 866 | -- Error message suppression
|
| 863 | 867 | | Opt_ShowErrorContext
|
| 864 | 868 |
| ... | ... | @@ -2468,6 +2468,7 @@ dFlagsDeps = [ |
| 2468 | 2468 | flagSpec "ppr-case-as-let" Opt_PprCaseAsLet,
|
| 2469 | 2469 | depFlagSpec' "ppr-ticks" Opt_PprShowTicks
|
| 2470 | 2470 | (\turn_on -> useInstead "-d" "suppress-ticks" (not turn_on)),
|
| 2471 | + flagSpec "stable-core-dump-order" Opt_StableCoreDumpOrder,
|
|
| 2471 | 2472 | flagSpec "suppress-ticks" Opt_SuppressTicks,
|
| 2472 | 2473 | depFlagSpec' "suppress-stg-free-vars" Opt_SuppressStgExts
|
| 2473 | 2474 | (useInstead "-d" "suppress-stg-exts"),
|
| ... | ... | @@ -422,6 +422,7 @@ data SDocContext = SDC |
| 422 | 422 | , sdocSuppressModulePrefixes :: !Bool
|
| 423 | 423 | , sdocSuppressStgExts :: !Bool
|
| 424 | 424 | , sdocSuppressStgReps :: !Bool
|
| 425 | + , sdocStableCoreDumpOrder :: !Bool
|
|
| 425 | 426 | , sdocErrorSpans :: !Bool
|
| 426 | 427 | , sdocStarIsType :: !Bool
|
| 427 | 428 | , sdocLinearTypes :: !Bool
|
| ... | ... | @@ -490,6 +491,7 @@ defaultSDocContext = SDC |
| 490 | 491 | , sdocSuppressModulePrefixes = False
|
| 491 | 492 | , sdocSuppressStgExts = False
|
| 492 | 493 | , sdocSuppressStgReps = True
|
| 494 | + , sdocStableCoreDumpOrder = False
|
|
| 493 | 495 | , sdocErrorSpans = False
|
| 494 | 496 | , sdocStarIsType = False
|
| 495 | 497 | , sdocLinearTypes = False
|
| ... | ... | @@ -959,6 +959,33 @@ parts that you are not interested in. |
| 959 | 959 | has shown you where to look, you can try again without
|
| 960 | 960 | :ghc-flag:`-dsuppress-uniques`
|
| 961 | 961 | |
| 962 | +.. ghc-flag:: -dstable-core-dump-order
|
|
| 963 | + :shortdesc: Reorder top-level bindings in Core dumps into a stable,
|
|
| 964 | + diffable order
|
|
| 965 | + :type: dynamic
|
|
| 966 | + :reverse: -dno-stable-core-dump-order
|
|
| 967 | + :category: verbosity
|
|
| 968 | + |
|
| 969 | + :since: FIXME
|
|
| 970 | + |
|
| 971 | + Normally the order of top-level bindings in a Core dump (such as the
|
|
| 972 | + output of :ghc-flag:`-ddump-simpl`) reflects the compiler's internal
|
|
| 973 | + processing order, which depends on ``Unique`` values. Those uniques can
|
|
| 974 | + shift whenever an unrelated upstream module changes, so the bindings get
|
|
| 975 | + re-ordered and a textual ``diff`` of two dumps fails to line up the real
|
|
| 976 | + changes.
|
|
| 977 | + |
|
| 978 | + This flag is opt-in and reorders the top-level bindings of Core dumps that
|
|
| 979 | + go through the pass-result printer (e.g. :ghc-flag:`-ddump-simpl`,
|
|
| 980 | + :ghc-flag:`-ddump-prep`, :ghc-flag:`-ddump-ds`,
|
|
| 981 | + :ghc-flag:`-ddump-simpl-iterations`) into a stable, source-location-driven
|
|
| 982 | + order that does not depend on uniques.
|
|
| 983 | + |
|
| 984 | + It is intended to be combined with :ghc-flag:`-dsuppress-uniques` when
|
|
| 985 | + diffing two dumps, but because the ordering does not depend on uniques the
|
|
| 986 | + output is also more diffable without it. The default (in-compiler) order is
|
|
| 987 | + retained because it is useful when debugging the compiler itself.
|
|
| 988 | + |
|
| 962 | 989 | .. ghc-flag:: -dsuppress-idinfo
|
| 963 | 990 | :shortdesc: Suppress extended information about identifiers where they
|
| 964 | 991 | are bound
|
| ... | ... | @@ -298,3 +298,15 @@ T17901: |
| 298 | 298 | $(RM) -f T17901.o T17901.hi
|
| 299 | 299 | '$(TEST_HC)' $(TEST_HC_OPTS) -O -c -ddump-simpl -dsuppress-uniques T17901.hs | grep 'wombat'
|
| 300 | 300 | # All three functions should get their case alternatives combined
|
| 301 | + |
|
| 302 | +# T27296: check -dstable-core-dump-order on a small Data.Map-style module. The
|
|
| 303 | +# sed allow-list prints, deduplicated, the top-level binders we care about in
|
|
| 304 | +# dump order. It inspects names only, so it is insensitive to unrelated
|
|
| 305 | +# Core-format churn.
|
|
| 306 | +T27296:
|
|
| 307 | + $(RM) -f T27296.o T27296.hi
|
|
| 308 | + '$(TEST_HC)' $(TEST_HC_OPTS) -O -c -ddump-simpl -dsuppress-uniques \
|
|
| 309 | + -dsuppress-idinfo -dsuppress-module-prefixes -dno-typeable-binds \
|
|
| 310 | + -dstable-core-dump-order T27296.hs 2> /dev/null \
|
|
| 311 | + | sed -nE 's/^(\$$fEqKey|\$$fOrdKey|\$$fOrdKey_\$$ccompare|size|lookupG|member|findI|insertG|insertManyI|insertTwoI|weight|balance|ratios|fromAscI|\$$wweight|findI_\$$slookupG|fromAscI_\$$sinsertG|lvl)( .*)?$$/\1/p' \
|
|
| 312 | + | uniq |
| 1 | +{-# LANGUAGE BangPatterns #-}
|
|
| 2 | + |
|
| 3 | +-- See #27296 and Note [Stable Core dump order] in GHC.Core.Ppr.
|
|
| 4 | +--
|
|
| 5 | +-- A small Data.Map-style module exercising the trickier parts of the stable
|
|
| 6 | +-- dump ordering: under -O it produces specialisations ($slookupG, $sinsertG),
|
|
| 7 | +-- a worker ($wweight), a floated lvl, and derived Eq/Ord instances for a custom
|
|
| 8 | +-- Key type, alongside the user functions. The source order is deliberately
|
|
| 9 | +-- neither alphabetical nor the default dump order (insertG forward-references
|
|
| 10 | +-- balance), so the test pins source-position ordering specifically.
|
|
| 11 | +module T27296
|
|
| 12 | + ( Key(..), size, lookupG, member, findI, insertG, insertManyI, insertTwoI
|
|
| 13 | + , weight, balance, ratios, fromAscI )
|
|
| 14 | + where
|
|
| 15 | + |
|
| 16 | +-- A custom key with a derived Ord instance: the derived $fEqKey/$fOrdKey
|
|
| 17 | +-- binders inherit this declaration's source span, so they cluster here.
|
|
| 18 | +data Key = Key Int deriving (Eq, Ord)
|
|
| 19 | + |
|
| 20 | +data Map k a = Tip | Bin !Int k a !(Map k a) !(Map k a)
|
|
| 21 | + |
|
| 22 | +data Sizes = Sizes !Int !Int
|
|
| 23 | + |
|
| 24 | +size :: Map k a -> Int
|
|
| 25 | +size Tip = 0
|
|
| 26 | +size (Bin sz _ _ _ _) = sz
|
|
| 27 | + |
|
| 28 | +lookupG :: Ord k => k -> Map k a -> Maybe a
|
|
| 29 | +lookupG _ Tip = Nothing
|
|
| 30 | +lookupG k (Bin _ kx x l r) = case compare k kx of
|
|
| 31 | + LT -> lookupG k l
|
|
| 32 | + GT -> lookupG k r
|
|
| 33 | + EQ -> Just x
|
|
| 34 | +{-# SPECIALISE lookupG :: Key -> Map Key a -> Maybe a #-}
|
|
| 35 | + |
|
| 36 | +member :: Key -> Map Key a -> Bool
|
|
| 37 | +member k m = case lookupG k m of
|
|
| 38 | + Nothing -> False
|
|
| 39 | + Just _ -> True
|
|
| 40 | + |
|
| 41 | +findI :: Key -> Map Key a -> a -> a
|
|
| 42 | +findI k m def = case lookupG k m of
|
|
| 43 | + Nothing -> def
|
|
| 44 | + Just v -> v
|
|
| 45 | + |
|
| 46 | +-- insertG references 'balance', which is defined further down (forward ref).
|
|
| 47 | +insertG :: Ord k => k -> a -> Map k a -> Map k a
|
|
| 48 | +insertG k x Tip = Bin 1 k x Tip Tip
|
|
| 49 | +insertG k x (Bin sz kx kv l r) = case compare k kx of
|
|
| 50 | + LT -> balance kx kv (insertG k x l) r
|
|
| 51 | + GT -> balance kx kv l (insertG k x r)
|
|
| 52 | + EQ -> Bin sz k x l r
|
|
| 53 | +{-# SPECIALISE insertG :: Key -> a -> Map Key a -> Map Key a #-}
|
|
| 54 | + |
|
| 55 | +insertManyI :: [(Key, a)] -> Map Key a -> Map Key a
|
|
| 56 | +insertManyI xs m0 = foldr (\(k, x) m -> insertG k x m) m0 xs
|
|
| 57 | + |
|
| 58 | +insertTwoI :: Key -> Key -> a -> Map Key a
|
|
| 59 | +insertTwoI k1 k2 x = insertG k1 x (insertG k2 x Tip)
|
|
| 60 | + |
|
| 61 | +-- weight unboxes the strict fields of Sizes -> worker/wrapper $wweight.
|
|
| 62 | +weight :: Sizes -> Int
|
|
| 63 | +weight (Sizes a b) = a * a + 3 * b * b + a * b + 1
|
|
| 64 | + |
|
| 65 | +balance :: k -> a -> Map k a -> Map k a -> Map k a
|
|
| 66 | +balance k x l r = Bin (weight (Sizes sl sr)) k x l r
|
|
| 67 | + where
|
|
| 68 | + sl = size l
|
|
| 69 | + sr = size r
|
|
| 70 | + |
|
| 71 | +-- baseRatios is a closed constant under a lambda -> floated to a top-level lvl.
|
|
| 72 | +ratios :: Int -> [Int]
|
|
| 73 | +ratios n = map (n +) baseRatios
|
|
| 74 | + where baseRatios = [2, 3, 5, 7, 11, 13]
|
|
| 75 | + |
|
| 76 | +fromAscI :: [(Key, a)] -> Map Key a
|
|
| 77 | +fromAscI = foldr (\(k, x) m -> insertG k x m) Tip |
| 1 | +$fEqKey
|
|
| 2 | +$fOrdKey
|
|
| 3 | +$fOrdKey_$ccompare
|
|
| 4 | +size
|
|
| 5 | +findI_$slookupG
|
|
| 6 | +lookupG
|
|
| 7 | +member
|
|
| 8 | +findI
|
|
| 9 | +fromAscI_$sinsertG
|
|
| 10 | +insertG
|
|
| 11 | +insertManyI
|
|
| 12 | +insertTwoI
|
|
| 13 | +$wweight
|
|
| 14 | +weight
|
|
| 15 | +balance
|
|
| 16 | +ratios
|
|
| 17 | +fromAscI
|
|
| 18 | +lvl |
| ... | ... | @@ -602,3 +602,4 @@ test('T25718b', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress |
| 602 | 602 | test('T25718c', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress-all -dno-typeable-binds'])
|
| 603 | 603 | test('T19166', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress-all -dno-typeable-binds'])
|
| 604 | 604 | test('T27261', [extra_files(['T27261_aux.hs'])], multimod_compile, ['T27261', '-v0 -O'])
|
| 605 | +test('T27296', [], makefile_test, ['T27296']) |