Marge Bot pushed to branch wip/marge_bot_batch_merge_job at Glasgow Haskell Compiler / GHC Commits: f9bcfac2 by sheaf at 2026-06-03T14:47:19-04:00 Avoid mkTick in Core Prep breaking ANF As discovered in #27182, mkTick can break ANF. This patch introduces a variant of mkTick that skips the single optimisation that could break ANF. This is preferrable over switching to the raw Tick constructor, as the latter may introduce spurious cost centres in profiling reports. This is a temporary measure until we more thoroughly refactor how mkTick works (see #27141). See Note [mkTick breaks ANF] in GHC.CoreToStg.Prep. Fixes #27182 - - - - - cf1fd661 by Artem Pelenitsyn at 2026-06-03T14:48:09-04:00 clarify comment for getSizeofMutableByteArray#: we get the size in bytes, not "elements" - - - - - 4b4aba00 by sheaf at 2026-06-04T07:54:22-04:00 Fix AArch64 clobbering bug for MUL2 On AArch64, the code generator could clobber one of the input operands when computing the lower bits of a MUL2 operation. This rendered invalid the subsequent computation of the high bits. This commit fixes that by using a temporary register. The register allocator can remove the redundant move in the common case when the registers do not conflict. Fixes #27046 - - - - - c19aa850 by Simon Jakobi at 2026-06-04T07:54:23-04:00 testsuite: Deduplicate --only test names config.only is assumed to be a set, but supplying --only overwrote it with the (list) argparse result, which can contain duplicates. When a test ran, config.only.remove(name) dropped only the first occurrence, so a duplicated name lingered and was later misreported as a "test not found" framework failure. Store it as a set instead. Fixes #27322 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> - - - - - 12 changed files: - + changelog.d/T27046 - + changelog.d/T27182.md - compiler/GHC/Builtin/primops.txt.pp - compiler/GHC/CmmToAsm/AArch64/CodeGen.hs - compiler/GHC/Core/Utils.hs - compiler/GHC/CoreToStg/Prep.hs - testsuite/driver/runtests.py - + testsuite/tests/codeGen/should_run/T27046.hs - + testsuite/tests/codeGen/should_run/T27046_cmm.cmm - testsuite/tests/codeGen/should_run/all.T - + testsuite/tests/profiling/should_compile/T27182.hs - testsuite/tests/profiling/should_compile/all.T Changes: ===================================== changelog.d/T27046 ===================================== @@ -0,0 +1,9 @@ +section: compiler +issues: #27046 +mrs: !16031 +synopsis: + Avoid AArch64 register clobbering bug in MUL2 +description: + Fixes an issue in which, on AArch64, code generation for the MUL2 operation + could clobber one of the input operands when computing the lower bits, which + rendered invalid the subsequent computation of the high bits. ===================================== changelog.d/T27182.md ===================================== @@ -0,0 +1,8 @@ +section: compiler +issues: #27182 +mrs: !16003 +synopsis: + Fix `getStgArgFromTrivialArg` panic in CoreToStg +description: + Avoid a `getStgArgFromTrivialArg` panic in CoreToStg due to a call to `mkTick` + in Core Prep which invalidated ANF. ===================================== compiler/GHC/Builtin/primops.txt.pp ===================================== @@ -2149,7 +2149,7 @@ primop SizeofMutableByteArrayOp "sizeofMutableByteArray#" GenPrimOp primop GetSizeofMutableByteArrayOp "getSizeofMutableByteArray#" GenPrimOp MutableByteArray# s -> State# s -> (# State# s, Int# #) - {Return the number of elements in the array, correctly accounting for + {Return the number of bytes in the array, correctly accounting for the effect of 'shrinkMutableByteArray#' and 'resizeMutableByteArray#'. @since 0.5.0.0} ===================================== compiler/GHC/CmmToAsm/AArch64/CodeGen.hs ===================================== @@ -2300,11 +2300,19 @@ genCCall target dest_regs arg_regs = do let lo = getRegisterReg platform (CmmLocal dst_lo) hi = getRegisterReg platform (CmmLocal dst_hi) nd = getRegisterReg platform (CmmLocal dst_needed) + + -- Generate a fresh virtual register for the low word computation. + -- This avoids clobbering reg_a or reg_b in the first MUL instruction, + -- which could for example happen if 'lo' and 'reg_a' are the same + -- virtual register. + tmp_lo <- getNewRegNat II64 + return $ code_x `appOL` code_y `snocOL` - MUL II64 (OpReg W64 lo) (OpReg W64 reg_a) (OpReg W64 reg_b) `snocOL` + MUL II64 (OpReg W64 tmp_lo) (OpReg W64 reg_a) (OpReg W64 reg_b) `snocOL` SMULH (OpReg W64 hi) (OpReg W64 reg_a) (OpReg W64 reg_b) `snocOL` + MOV (OpReg W64 lo) (OpReg W64 tmp_lo) `snocOL` -- Are all high bits equal to the sign bit of the low word? -- nd = (hi == ASR(lo,width-1)) ? 1 : 0 CMP (OpReg W64 hi) (OpRegShift W64 lo SASR (widthInBits w - 1)) `snocOL` ===================================== compiler/GHC/Core/Utils.hs ===================================== @@ -11,6 +11,7 @@ module GHC.Core.Utils ( -- * Constructing expressions mkCast, mkCastMCo, mkPiMCo, mkTick, mkTicks, mkTickNoHNF, tickHNFArgs, + mkTickCpe, bindNonRec, needsCaseBinding, needsCaseBindingL, mkAltExpr, mkDefaultCase, mkSingleAltCase, @@ -319,7 +320,18 @@ mkCast expr co -- * Split profiling ticks into counting/scoping parts so that the two parts -- can be placed independently into the AST. mkTick :: CoreTickish -> CoreExpr -> CoreExpr -mkTick t orig_expr = mkTick' orig_expr +mkTick = mk_tick False + +-- | A version of 'mkTick' that preserves ANF, for use in Core Prep. +-- +-- See Note [mkTick breaks ANF] in GHC.CoreToStg.Prep. +mkTickCpe :: CoreTickish -> CoreExpr -> CoreExpr +mkTickCpe = mk_tick True + +-- | Internal function used to define both 'mkTick' and 'mkTickCpe' +-- without duplication. +mk_tick :: Bool -> CoreTickish -> CoreExpr -> CoreExpr +mk_tick preserve_anf t orig_expr = mkTick' orig_expr where -- Some ticks (cost-centres) can be split in two, with the -- non-counting part having laxer placement properties. @@ -343,7 +355,7 @@ mkTick t orig_expr = mkTick' orig_expr -- Push SCCs into lambdas. -- See (PSCC2) in Note [Pushing SCCs inwards]. | can_split - -> Tick (mkNoScope t) $ Lam x $ mkTick (mkNoCount t) e + -> Tick (mkNoScope t) $ Lam x $ mk_tick preserve_anf (mkNoCount t) e App f arg -- All ticks float inwards through non-runtime arguments, as per @@ -353,7 +365,9 @@ mkTick t orig_expr = mkTick' orig_expr -- Push SCCs into saturated constructor applications. -- See (PSCC3) in Note [Pushing SCCs inwards]. - | isSaturatedConApp expr + | not preserve_anf -- this optimisation breaks ANF; + -- see Note [mkTick breaks ANF] in GHC.CoreToStg.Prep + , isSaturatedConApp expr , tickishPlace t == PlaceCostCentre || can_split -> if tickishPlace t == PlaceCostCentre then tickHNFArgs t expr ===================================== compiler/GHC/CoreToStg/Prep.hs ===================================== @@ -801,7 +801,9 @@ cpeBodyF env (Tick tickish expr) ; return (FloatTick tickish `consFloat` floats, body) } | otherwise = do { body <- cpeBody env expr - ; return (emptyFloats, mkTick tickish' body) } + ; return (emptyFloats, mkTickCpe tickish' body) } + -- Use mkTickCpe and not mkTick, as the latter may break ANF (#27182). + -- See (TickANF2) in Note [mkTick breaks ANF]. where tickish' | Breakpoint ext bid fvs <- tickish -- See also 'substTickish' @@ -905,6 +907,28 @@ cpeBodyF env (Case scrut bndr ty alts) ; rhs' <- cpeBody env2 rhs ; return (Alt con bs' rhs') } +{- Note [mkTick breaks ANF] +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +mkTick does not preserve the ANF property as required by Core Prep (see +Note [CorePrep invariants]), as seen in #27182. Given: + + mkTick scc<foo> (\ (eta :: Char -> Bool) -> BindP (p :: Int) eta) + +mkTick will push the SCC into the constructor application, resulting in: + + \ (eta :: Char -> Bool) -> BindP (p :: Int) (scc<oneM> eta) + +To avoid this problem (at least until 'mkTick' is more thoroughly reworked to +avoid this infelicity, see #27141), we define a variant of 'mkTick', called +'mkTickCpe', which does not push ticks into constructor applications (this is +the only optimisation done by 'mkTick' that can break ANF). + +We prefer using a small variant of 'mkTick' rather than using the 'Tick' +constructor, as the latter can slightly degrade profiling reports by failing to +combine ticks (can result in spurious cost centres with 0 entries appearing in +profiling reports). +-} + -- --------------------------------------------------------------------------- -- CpeBody: produces a result satisfying CpeBody -- --------------------------------------------------------------------------- @@ -1207,7 +1231,7 @@ cpeApp top_env expr rebuild_app' env (a : as) fun' floats ss rt_ticks req_depth = case a of -- See Note [Ticks and mandatory eta expansion] _ | not (null rt_ticks), req_depth <= 0 - -> let tick_fun = foldr mkTick fun' rt_ticks + -> let tick_fun = foldr mkTickCpe fun' rt_ticks in rebuild_app' env (a : as) tick_fun floats ss rt_ticks req_depth AIApp (Type arg_ty) @@ -2307,7 +2331,7 @@ wrapBinds floats body mk_bind (UnsafeEqualityCase scrut b con bs) body = mkSingleAltCase scrut b con bs body mk_bind (FloatTick tickish) body - = mkTick tickish body + = mkTickCpe tickish body -- | Put floats at top-level deFloatTop :: Floats -> [CoreBind] @@ -2735,7 +2759,7 @@ newVar env ty wrapTicks :: Floats -> CoreExpr -> (Floats, CoreExpr) wrapTicks floats expr | (floats1, ticks1) <- fold_fun go floats - = (floats1, foldrOL mkTick expr ticks1) + = (floats1, foldrOL mkTickCpe expr ticks1) where fold_fun f floats = let (binds, ticks) = foldlOL f (nilOL,nilOL) (fs_binds floats) in (floats { fs_binds = binds }, ticks) @@ -2755,8 +2779,8 @@ wrapTicks floats expr wrap t (Float bind bound info) = Float (wrapBind t bind) bound info wrap _ f = pprPanic "Unexpected FloatingBind" (ppr f) - wrapBind t (NonRec binder rhs) = NonRec binder (mkTick t rhs) - wrapBind t (Rec pairs) = Rec (mapSnd (mkTick t) pairs) + wrapBind t (NonRec binder rhs) = NonRec binder (mkTickCpe t rhs) + wrapBind t (Rec pairs) = Rec (mapSnd (mkTickCpe t) pairs) ------------------------------------------------------------------------------ -- Numeric literals ===================================== testsuite/driver/runtests.py ===================================== @@ -133,7 +133,7 @@ if args.unexpected_output_dir: config.unexpected_output_dir = Path(args.unexpected_output_dir) if args.only: - config.only = args.only + config.only = set(args.only) config.run_only_some_tests = True if args.skip: ===================================== testsuite/tests/codeGen/should_run/T27046.hs ===================================== @@ -0,0 +1,29 @@ +{-# LANGUAGE MagicHash #-} +{-# LANGUAGE ForeignFunctionInterface, GHCForeignImportPrim, UnliftedFFITypes #-} + +module Main where + +import Control.Monad + ( unless ) +import Data.Bits + ( shiftL ) +import GHC.Exts + ( Int64# ) +import GHC.Int + ( Int64(..) ) + +foreign import prim "test_mul2_clobber" + test_mul2_clobber :: Int64# -> Int64# -> Int64# + +main :: IO () +main = do + let + I64# x = 1 `shiftL` 32 + hi = I64# $ test_mul2_clobber x x + + unless ( hi == 1 ) $ + error $ unlines + [ "Incorrect result for Mul2 operation." + , "Expected high word: 1" + , " Actual high word: " ++ show hi + ] ===================================== testsuite/tests/codeGen/should_run/T27046_cmm.cmm ===================================== @@ -0,0 +1,13 @@ +#include "Cmm.h" + +// Test for #27046 +test_mul2_clobber (bits64 x, bits64 y) +{ + bits64 hi, nd; + + // Deliberately alias the destination 'lo' with the source 'x' + // This forces the NCG to use the same virtual register for both. + (nd, hi, x) = prim %mul2_64(x, y); + + return (hi); +} ===================================== testsuite/tests/codeGen/should_run/all.T ===================================== @@ -260,6 +260,7 @@ test('T25364', normal, compile_and_run, ['']) test('T26061', normal, compile_and_run, ['']) test('T26537', normal, compile_and_run, ['-O2 -fregs-graph']) test('T24016', normal, compile_and_run, ['-O1 -fPIC']) +test('T27046', [req_cmm], compile_and_run, ['T27046_cmm.cmm']) # Check that GHC-generated finalizers run on Darwin. The Apple linker doesn't # support --wrap, so we can't intercept hs_spt_remove directly. Instead we ===================================== testsuite/tests/profiling/should_compile/T27182.hs ===================================== @@ -0,0 +1,6 @@ +module T27182 ( oneM ) where + +data Parser = BindP Int ( Char -> Bool ) + +oneM :: Int -> ( Char -> Bool ) -> Parser +oneM p = BindP p ===================================== testsuite/tests/profiling/should_compile/all.T ===================================== @@ -22,3 +22,4 @@ test('T19894', [test_opts, extra_files(['T19894'])], multimod_compile, ['Main', test('T20938', [test_opts], compile, ['-O -prof']) test('T26056', [test_opts], compile, ['-O -prof']) test('T27121', [test_opts, extra_files(['T27121_aux.hs'])], multimod_compile, ['T27121', '-v0 -O -prof -fprof-auto']) +test('T27182', [test_opts], compile, ['-O -prof -fprof-late']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/bc8fcb441cbcafd214c80761db298b7... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/bc8fcb441cbcafd214c80761db298b7... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Marge Bot (@marge-bot)