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
Add -dstable-core-dump-order for stable Core dump ordering (#27296)
The order of top-level bindings in Core dumps (-ddump-simpl etc.) is the
compiler's internal processing order, which is sensitive to Uniques.
Uniques can shift whenever an unrelated upstream module changes, so the
bindings get re-ordered and a textual diff of two dumps fails to line up
the real changes.
This adds an opt-in flag -dstable-core-dump-order that reorders the
top-level bindings of Core dumps routed through dumpPassResult into a
stable, unique-independent order: by source location, then a $-rank so a
derived $w/$s binder sorts before its origin (mirroring GHC's default
dependency order, where the wrapper calls the worker), then the OccName.
Workers and specialisations inherit their origin's source span, so they
cluster next to the binding they come from; anonymous floats (noSrcSpan)
sort to the end; Rec groups are kept intact. The default order is
retained as it is useful when debugging the compiler itself.
The flag is unique-independent, so it improves diffability with or
without -dsuppress-uniques. See Note [Stable Core dump order] in
GHC.Core.Ppr.
Adds test T27296, a small Data.Map-style module whose binders GHC emits
in a non-source order by default, asserting they come out stably ordered
under the flag.
Co-Authored-By: Claude Opus 4.7
- - - - -
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:
=====================================
changelog.d/stable-core-dump-order-27296
=====================================
@@ -0,0 +1,4 @@
+section: compiler
+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.
+issues: #27296
+mrs: !16143
=====================================
compiler/GHC/Core/Lint.hs
=====================================
@@ -327,12 +327,17 @@ dumpPassResult logger dump_core_sizes name_ppr_ctx mb_flag hdr extra_info binds
where
size_doc = sep [text "Result size of" <+> text hdr, nest 2 (equals <+> ppr (coreBindsStats binds))]
+ -- See Note [Stable Core dump order] in GHC.Core.Ppr
+ binds' | sdocStableCoreDumpOrder (log_default_dump_context (logFlags logger))
+ = sortCoreBindingsForDump binds
+ | otherwise = binds
+
dump_doc = vcat [ nest 2 extra_info
, size_doc
, blankLine
, if dump_core_sizes
- then pprCoreBindingsWithSize binds
- else pprCoreBindings binds
+ then pprCoreBindingsWithSize binds'
+ else pprCoreBindings binds'
, ppUnless (null rules) pp_rules ]
pp_rules = vcat [ blankLine
, text "------ Local rules for imported ids --------"
=====================================
compiler/GHC/Core/Ppr.hs
=====================================
@@ -19,6 +19,7 @@ module GHC.Core.Ppr (
pprCoreExpr, pprParendExpr,
pprCoreBinding, pprCoreBindings, pprCoreAlt,
pprCoreBindingWithSize, pprCoreBindingsWithSize,
+ sortCoreBindingsForDump,
pprCoreBinder, pprCoreBinders, pprId, pprIds,
pprRule, pprRules, pprOptCo,
pprOcc, pprOccWithTick
@@ -30,7 +31,7 @@ import GHC.Core
import GHC.Core.Stats (exprStats)
import GHC.Types.Fixity (LexicalFixity(..))
import GHC.Types.Literal( pprLiteral )
-import GHC.Types.Name( pprInfixName, pprPrefixName )
+import GHC.Types.Name( pprInfixName, pprPrefixName, getOccString, getSrcSpan )
import GHC.Types.Var
import GHC.Types.Id
import GHC.Types.Id.Info
@@ -44,9 +45,13 @@ import GHC.Core.Coercion
import GHC.Types.Basic
import GHC.Utils.Misc
import GHC.Utils.Outputable
-import GHC.Types.SrcLoc ( pprUserRealSpan )
+import GHC.Utils.Panic (panic)
+import GHC.Types.SrcLoc ( SrcSpan(..), srcSpanStartLine, srcSpanStartCol
+ , pprUserRealSpan )
import GHC.Types.Tickish
+import Data.List ( sortOn )
+
{-
************************************************************************
* *
@@ -71,6 +76,70 @@ pprCoreBindingWithSize :: CoreBind -> SDoc
pprCoreBindingsWithSize = pprTopBinds sizeAnn
pprCoreBindingWithSize = pprTopBind sizeAnn
+{- Note [Stable Core dump order]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The order of top-level bindings in a Core dump (-ddump-simpl etc.) is the
+compiler's internal processing order, which is sensitive to Uniques. Uniques
+can shift whenever an unrelated upstream module changes, so the bindings get
+re-ordered and a textual diff of two dumps fails to line up the real changes
+(#27296).
+
+With -dstable-core-dump-order we reorder the top-level bindings at dump time into
+a stable, diffable order. 'sortCoreBindingsForDump' sorts by a key that is
+*independent of Uniques* (so the output is diffable even without
+-dsuppress-uniques):
+
+ 1. the binder's source span (real spans in source order; noSrcSpan last).
+ Workers and specialisations inherit their origin's source span (see
+ 'mkWorkerId' and 'newSpecIdSM'), so they cluster next to the binding they
+ come from.
+ 2. a "$-rank" so that within one source span the compiler-derived binders sort
+ *before* the origin they come from (e.g. @$wfoo@ before @foo@), mirroring
+ GHC's default dependency order (the wrapper calls the worker, so the worker
+ comes first; specialisations likewise precede their origin). We rank by
+ whether the OccName *contains* a '$', which marks a derived binder: a worker
+ is @$wfoo@, but a call-site specialisation is tidied to @bar_$sfoo@ (no
+ leading '$'), so a leading-'$' test would miss it.
+ 3. the OccName string, as a final lexical, deterministic tie-break.
+
+Recursive groups are never split: a 'Rec' is one 'CoreBind', placed as a unit by
+its earliest-source member, with its members sorted by the same key.
+
+-dstable-core-dump-order is opt-in; the default order is retained because it is
+useful for debugging the compiler itself.
+-}
+
+-- | Reorder a 'CoreProgram' into a stable, source-location-driven order for
+-- dumping. See Note [Stable Core dump order]. Used by 'dumpPassResult' when
+-- -dstable-core-dump-order is enabled.
+sortCoreBindingsForDump :: CoreProgram -> CoreProgram
+sortCoreBindingsForDump = sortOn bindKey . map sortRecMembers
+ where
+ sortRecMembers (Rec prs) = Rec (sortOn (bndrKey . fst) prs)
+ sortRecMembers b = b
+
+ -- 'sortRecMembers' runs first, so a 'Rec' is already sorted by 'bndrKey'
+ -- when 'bindKey' sees it; its first member is therefore the minimum key.
+ bindKey :: CoreBind -> (Int, Int, Int, Int, String)
+ bindKey (NonRec b _) = bndrKey b
+ bindKey (Rec ((b,_):_)) = bndrKey b
+ bindKey (Rec []) = panic "sortCoreBindingsForDump: empty Rec"
+
+ bndrKey :: CoreBndr -> (Int, Int, Int, Int, String)
+ bndrKey b = (bucket, line, col, dollar_rank, s)
+ where
+ s = getOccString b
+ (bucket, line, col) = case getSrcSpan b of
+ RealSrcSpan rs _ -> (0, srcSpanStartLine rs, srcSpanStartCol rs)
+ _ -> (1, 0, 0) -- noSrcSpan: sort last
+ -- A '$' anywhere in a tidied top-level OccName marks a compiler-derived
+ -- binder ($wfoo, but also call-site specialisations tidied to
+ -- bar_$sfoo); rank those before their origin within a shared source span,
+ -- mirroring GHC's default dependency order (the wrapper calls the worker,
+ -- so the worker comes first).
+ dollar_rank | '$' `elem` s = 0
+ | otherwise = 1
+
instance OutputableBndr b => Outputable (Bind b) where
ppr bind = ppr_bind noAnn bind
=====================================
compiler/GHC/Driver/DynFlags.hs
=====================================
@@ -1594,6 +1594,7 @@ initSDocContext dflags style = SDC
, sdocSuppressModulePrefixes = gopt Opt_SuppressModulePrefixes dflags
, sdocSuppressStgExts = gopt Opt_SuppressStgExts dflags
, sdocSuppressStgReps = gopt Opt_SuppressStgReps dflags
+ , sdocStableCoreDumpOrder = gopt Opt_StableCoreDumpOrder dflags
, sdocErrorSpans = gopt Opt_ErrorSpans dflags
, sdocStarIsType = xopt LangExt.StarIsType dflags
, sdocLinearTypes = xopt LangExt.LinearTypes dflags
=====================================
compiler/GHC/Driver/Flags.hs
=====================================
@@ -859,6 +859,10 @@ data GeneralFlag
| Opt_SuppressTimestamps -- ^ Suppress timestamps in dumps
| Opt_SuppressCoreSizes -- ^ Suppress per binding Core size stats in dumps
+ -- | Reorder top-level bindings in Core dumps into a stable, diffable order.
+ -- See Note [Stable Core dump order] in GHC.Core.Ppr.
+ | Opt_StableCoreDumpOrder
+
-- Error message suppression
| Opt_ShowErrorContext
=====================================
compiler/GHC/Driver/Session.hs
=====================================
@@ -2468,6 +2468,7 @@ dFlagsDeps = [
flagSpec "ppr-case-as-let" Opt_PprCaseAsLet,
depFlagSpec' "ppr-ticks" Opt_PprShowTicks
(\turn_on -> useInstead "-d" "suppress-ticks" (not turn_on)),
+ flagSpec "stable-core-dump-order" Opt_StableCoreDumpOrder,
flagSpec "suppress-ticks" Opt_SuppressTicks,
depFlagSpec' "suppress-stg-free-vars" Opt_SuppressStgExts
(useInstead "-d" "suppress-stg-exts"),
=====================================
compiler/GHC/Utils/Outputable.hs
=====================================
@@ -422,6 +422,7 @@ data SDocContext = SDC
, sdocSuppressModulePrefixes :: !Bool
, sdocSuppressStgExts :: !Bool
, sdocSuppressStgReps :: !Bool
+ , sdocStableCoreDumpOrder :: !Bool
, sdocErrorSpans :: !Bool
, sdocStarIsType :: !Bool
, sdocLinearTypes :: !Bool
@@ -490,6 +491,7 @@ defaultSDocContext = SDC
, sdocSuppressModulePrefixes = False
, sdocSuppressStgExts = False
, sdocSuppressStgReps = True
+ , sdocStableCoreDumpOrder = False
, sdocErrorSpans = False
, sdocStarIsType = False
, sdocLinearTypes = False
=====================================
docs/users_guide/debugging.rst
=====================================
@@ -959,6 +959,33 @@ parts that you are not interested in.
has shown you where to look, you can try again without
:ghc-flag:`-dsuppress-uniques`
+.. ghc-flag:: -dstable-core-dump-order
+ :shortdesc: Reorder top-level bindings in Core dumps into a stable,
+ diffable order
+ :type: dynamic
+ :reverse: -dno-stable-core-dump-order
+ :category: verbosity
+
+ :since: FIXME
+
+ Normally the order of top-level bindings in a Core dump (such as the
+ output of :ghc-flag:`-ddump-simpl`) reflects the compiler's internal
+ processing order, which depends on ``Unique`` values. Those uniques can
+ shift whenever an unrelated upstream module changes, so the bindings get
+ re-ordered and a textual ``diff`` of two dumps fails to line up the real
+ changes.
+
+ This flag is opt-in and reorders the top-level bindings of Core dumps that
+ go through the pass-result printer (e.g. :ghc-flag:`-ddump-simpl`,
+ :ghc-flag:`-ddump-prep`, :ghc-flag:`-ddump-ds`,
+ :ghc-flag:`-ddump-simpl-iterations`) into a stable, source-location-driven
+ order that does not depend on uniques.
+
+ It is intended to be combined with :ghc-flag:`-dsuppress-uniques` when
+ diffing two dumps, but because the ordering does not depend on uniques the
+ output is also more diffable without it. The default (in-compiler) order is
+ retained because it is useful when debugging the compiler itself.
+
.. ghc-flag:: -dsuppress-idinfo
:shortdesc: Suppress extended information about identifiers where they
are bound
=====================================
testsuite/tests/simplCore/should_compile/Makefile
=====================================
@@ -298,3 +298,15 @@ T17901:
$(RM) -f T17901.o T17901.hi
'$(TEST_HC)' $(TEST_HC_OPTS) -O -c -ddump-simpl -dsuppress-uniques T17901.hs | grep 'wombat'
# All three functions should get their case alternatives combined
+
+# T27296: check -dstable-core-dump-order on a small Data.Map-style module. The
+# sed allow-list prints, deduplicated, the top-level binders we care about in
+# dump order. It inspects names only, so it is insensitive to unrelated
+# Core-format churn.
+T27296:
+ $(RM) -f T27296.o T27296.hi
+ '$(TEST_HC)' $(TEST_HC_OPTS) -O -c -ddump-simpl -dsuppress-uniques \
+ -dsuppress-idinfo -dsuppress-module-prefixes -dno-typeable-binds \
+ -dstable-core-dump-order T27296.hs 2> /dev/null \
+ | 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' \
+ | uniq
=====================================
testsuite/tests/simplCore/should_compile/T27296.hs
=====================================
@@ -0,0 +1,77 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- See #27296 and Note [Stable Core dump order] in GHC.Core.Ppr.
+--
+-- A small Data.Map-style module exercising the trickier parts of the stable
+-- dump ordering: under -O it produces specialisations ($slookupG, $sinsertG),
+-- a worker ($wweight), a floated lvl, and derived Eq/Ord instances for a custom
+-- Key type, alongside the user functions. The source order is deliberately
+-- neither alphabetical nor the default dump order (insertG forward-references
+-- balance), so the test pins source-position ordering specifically.
+module T27296
+ ( Key(..), size, lookupG, member, findI, insertG, insertManyI, insertTwoI
+ , weight, balance, ratios, fromAscI )
+ where
+
+-- A custom key with a derived Ord instance: the derived $fEqKey/$fOrdKey
+-- binders inherit this declaration's source span, so they cluster here.
+data Key = Key Int deriving (Eq, Ord)
+
+data Map k a = Tip | Bin !Int k a !(Map k a) !(Map k a)
+
+data Sizes = Sizes !Int !Int
+
+size :: Map k a -> Int
+size Tip = 0
+size (Bin sz _ _ _ _) = sz
+
+lookupG :: Ord k => k -> Map k a -> Maybe a
+lookupG _ Tip = Nothing
+lookupG k (Bin _ kx x l r) = case compare k kx of
+ LT -> lookupG k l
+ GT -> lookupG k r
+ EQ -> Just x
+{-# SPECIALISE lookupG :: Key -> Map Key a -> Maybe a #-}
+
+member :: Key -> Map Key a -> Bool
+member k m = case lookupG k m of
+ Nothing -> False
+ Just _ -> True
+
+findI :: Key -> Map Key a -> a -> a
+findI k m def = case lookupG k m of
+ Nothing -> def
+ Just v -> v
+
+-- insertG references 'balance', which is defined further down (forward ref).
+insertG :: Ord k => k -> a -> Map k a -> Map k a
+insertG k x Tip = Bin 1 k x Tip Tip
+insertG k x (Bin sz kx kv l r) = case compare k kx of
+ LT -> balance kx kv (insertG k x l) r
+ GT -> balance kx kv l (insertG k x r)
+ EQ -> Bin sz k x l r
+{-# SPECIALISE insertG :: Key -> a -> Map Key a -> Map Key a #-}
+
+insertManyI :: [(Key, a)] -> Map Key a -> Map Key a
+insertManyI xs m0 = foldr (\(k, x) m -> insertG k x m) m0 xs
+
+insertTwoI :: Key -> Key -> a -> Map Key a
+insertTwoI k1 k2 x = insertG k1 x (insertG k2 x Tip)
+
+-- weight unboxes the strict fields of Sizes -> worker/wrapper $wweight.
+weight :: Sizes -> Int
+weight (Sizes a b) = a * a + 3 * b * b + a * b + 1
+
+balance :: k -> a -> Map k a -> Map k a -> Map k a
+balance k x l r = Bin (weight (Sizes sl sr)) k x l r
+ where
+ sl = size l
+ sr = size r
+
+-- baseRatios is a closed constant under a lambda -> floated to a top-level lvl.
+ratios :: Int -> [Int]
+ratios n = map (n +) baseRatios
+ where baseRatios = [2, 3, 5, 7, 11, 13]
+
+fromAscI :: [(Key, a)] -> Map Key a
+fromAscI = foldr (\(k, x) m -> insertG k x m) Tip
=====================================
testsuite/tests/simplCore/should_compile/T27296.stdout
=====================================
@@ -0,0 +1,18 @@
+$fEqKey
+$fOrdKey
+$fOrdKey_$ccompare
+size
+findI_$slookupG
+lookupG
+member
+findI
+fromAscI_$sinsertG
+insertG
+insertManyI
+insertTwoI
+$wweight
+weight
+balance
+ratios
+fromAscI
+lvl
=====================================
testsuite/tests/simplCore/should_compile/all.T
=====================================
@@ -602,3 +602,4 @@ test('T25718b', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress
test('T25718c', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress-all -dno-typeable-binds'])
test('T19166', normal, compile, ['-O -ddump-simpl -dsuppress-uniques -dsuppress-all -dno-typeable-binds'])
test('T27261', [extra_files(['T27261_aux.hs'])], multimod_compile, ['T27261', '-v0 -O'])
+test('T27296', [], makefile_test, ['T27296'])
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/69b3d03abc04b5da1faa864059b58da4...
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/69b3d03abc04b5da1faa864059b58da4...
You're receiving this email because of your account on gitlab.haskell.org.