[Git][ghc/ghc][wip/nested-cpr-2019] Consider unboxing effects of WW better and get rid of hack
Sebastian Graf
gitlab at gitlab.haskell.org
Tue Apr 21 16:41:31 UTC 2020
Sebastian Graf pushed to branch wip/nested-cpr-2019 at Glasgow Haskell Compiler / GHC
Commits:
063b38a6 by Sebastian Graf at 2020-04-21T18:41:23+02:00
Consider unboxing effects of WW better and get rid of hack
- - - - -
3 changed files:
- compiler/GHC/Core/Arity.hs
- compiler/GHC/Core/Op/CprAnal.hs
- compiler/GHC/Types/Cpr.hs
Changes:
=====================================
compiler/GHC/Core/Arity.hs
=====================================
@@ -12,7 +12,7 @@
-- | Arity and eta expansion
module GHC.Core.Arity
- ( manifestArity, joinRhsArity, exprArity, typeArity
+ ( manifestArity, joinRhsArity, exprArity, typeArity, splitFunNewTys
, exprEtaExpandArity, findRhsArity, etaExpand
, etaExpandToJoinPoint, etaExpandToJoinPointRule
, exprBotStrictness_maybe
@@ -41,6 +41,7 @@ import GHC.Driver.Session ( DynFlags, GeneralFlag(..), gopt )
import Outputable
import FastString
import Util ( debugIsOn )
+import Maybes
{-
************************************************************************
@@ -117,33 +118,37 @@ typeArity :: Type -> [OneShotInfo]
-- How many value arrows are visible in the type?
-- We look through foralls, and newtypes
-- See Note [exprArity invariant]
-typeArity ty
- = go initRecTc ty
+typeArity ty = mapMaybe go (fst (splitPiNewTys ty))
where
- go rec_nts ty
- | Just (_, ty') <- splitForAllTy_maybe ty
- = go rec_nts ty'
-
- | Just (arg,res) <- splitFunTy_maybe ty
- = typeOneShot arg : go rec_nts res
+ -- Important to look through non-recursive newtypes, so that, eg
+ -- (f x) where f has arity 2, f :: Int -> IO ()
+ -- Here we want to get arity 1 for the result!
+ --
+ -- AND through a layer of recursive newtypes
+ -- e.g. newtype Stream m a b = Stream (m (Either b (a, Stream m a b)))
+ go ty_co_bndr = typeOneShot <$> binderRelevantType_maybe ty_co_bndr
+
+-- | Like 'splitFunTys', but this one also looks through newtypes and foralls.
+splitFunNewTys :: Type -> ([Type], Type)
+splitFunNewTys ty = (mapMaybe binderRelevantType_maybe arg_bndrs, res_ty)
+ where
+ (arg_bndrs, res_ty) = splitPiNewTys ty
+-- | Like 'splitPiTys', but this one also looks through newtypes.
+splitPiNewTys :: Type -> ([TyCoBinder], Type)
+splitPiNewTys ty = go initRecTc ty []
+ where
+ go rec_nts ty arg_tys
+ -- ForAllTys and FunTys
+ | Just (arg, res_ty) <- splitPiTy_maybe ty
+ = go rec_nts res_ty (arg:arg_tys)
+ -- See Note [Expanding newtypes] in GHC.Core.TyCon
| Just (tc,tys) <- splitTyConApp_maybe ty
, Just (ty', _) <- instNewTyCon_maybe tc tys
- , Just rec_nts' <- checkRecTc rec_nts tc -- See Note [Expanding newtypes]
- -- in GHC.Core.TyCon
--- , not (isClassTyCon tc) -- Do not eta-expand through newtype classes
--- -- See Note [Newtype classes and eta expansion]
--- (no longer required)
- = go rec_nts' ty'
- -- Important to look through non-recursive newtypes, so that, eg
- -- (f x) where f has arity 2, f :: Int -> IO ()
- -- Here we want to get arity 1 for the result!
- --
- -- AND through a layer of recursive newtypes
- -- e.g. newtype Stream m a b = Stream (m (Either b (a, Stream m a b)))
-
+ , Just rec_nts' <- checkRecTc rec_nts tc
+ = go rec_nts' ty' arg_tys
| otherwise
- = []
+ = (reverse arg_tys, ty)
---------------
exprBotStrictness_maybe :: CoreExpr -> Maybe (Arity, StrictSig)
=====================================
compiler/GHC/Core/Op/CprAnal.hs
=====================================
@@ -17,6 +17,7 @@ import GHC.Driver.Session
import GHC.Types.Demand
import GHC.Types.Cpr
import GHC.Core
+import GHC.Core.Arity ( splitFunNewTys )
import GHC.Core.Seq
import Outputable
import GHC.Types.Var.Env
@@ -206,7 +207,7 @@ cprAnal' env args (Case scrut case_bndr ty alts)
(whnf_flag, case_bndr_ty) = forceCprTy (getStrDmd seqDmd) scrut_ty
-- Regardless of whether scrut had the CPR property or not, the case binder
-- certainly has it. See 'extendEnvForDataAlt'.
- (alt_tys, alts') = mapAndUnzip (cprAnalAlt env args scrut case_bndr case_bndr_ty) alts
+ (alt_tys, alts') = mapAndUnzip (cprAnalAlt env args case_bndr case_bndr_ty) alts
res_ty = foldl' lubCprType botCprType alt_tys `bothCprType` whnf_flag
cprAnal' env args (Let (NonRec id rhs) body)
@@ -225,18 +226,17 @@ cprAnal' env args (Let (Rec pairs) body)
cprAnalAlt
:: AnalEnv
-> [CprType] -- ^ info about incoming arguments
- -> CoreExpr -- ^ scrutinee
-> Id -- ^ case binder
-> CprType -- ^ info about the case binder
-> Alt Var -- ^ current alternative
-> (CprType, Alt Var)
-cprAnalAlt env args scrut case_bndr case_bndr_ty (con@(DataAlt dc),bndrs,rhs)
+cprAnalAlt env args case_bndr case_bndr_ty (con@(DataAlt dc),bndrs,rhs)
-- See 'extendEnvForDataAlt' and Note [CPR in a DataAlt case alternative]
= (rhs_ty, (con, bndrs, rhs'))
where
- env_alt = extendEnvForDataAlt env scrut case_bndr case_bndr_ty dc bndrs
+ env_alt = extendEnvForDataAlt env case_bndr case_bndr_ty dc bndrs
(rhs_ty, rhs') = cprAnal env_alt args rhs
-cprAnalAlt env args _ case_bndr case_bndr_ty (con,bndrs,rhs)
+cprAnalAlt env args case_bndr case_bndr_ty (con,bndrs,rhs)
= (rhs_ty, (con, bndrs, rhs'))
where
env' = extendAnalEnv env case_bndr (CprSig case_bndr_ty)
@@ -330,6 +330,18 @@ pruneSig d (CprSig cpr_ty)
-- behavior.
= CprSig $ cpr_ty { ct_cpr = pruneDeepCpr d (ct_cpr cpr_ty `lubCpr` initRecFunCpr) }
+unboxingStrategy :: AnalEnv -> UnboxingStrategy
+unboxingStrategy env ty dmd
+ = prj <$> wantToUnbox (ae_fam_envs env) has_inlineable_prag ty dmd
+ where
+ prj (dmds, DataConAppContext { dcac_dc = dc, dcac_arg_tys = tys_w_str })
+ = (dc, map fst tys_w_str, dmds)
+ -- Rather than maintaining in AnalEnv whether we are in an INLINEABLE
+ -- function, we just assume that we aren't. That flag is only relevant
+ -- to Note [Do not unpack class dictionaries], the few unboxing
+ -- opportunities on dicts it prohibits are probably irrelevant to CPR.
+ has_inlineable_prag = False
+
-- | Process the RHS of the binding for a sensible arity, add the CPR signature
-- to the Id, and augment the environment with the signature as well.
cprAnalBind
@@ -342,20 +354,23 @@ cprAnalBind
cprAnalBind top_lvl env args id rhs
= (id', rhs')
where
+ arg_tys = fst (splitFunNewTys (idType id))
-- We compute the Termination and CPR transformer based on the strictness
-- signature. There is no point in pretending that an arg we are strict in
-- could lead to non-termination, as the signature then trivially
-- MightDiverge. Instead we assume that call sites make sure to force the
-- arguments appropriately and unleash the TerminationFlag there.
- assumed_arg_tys = argCprTypesFromStrictSig (idStrictness id)
+ assumed_arg_cpr_tys = argCprTypesFromStrictSig (unboxingStrategy env)
+ arg_tys
+ (idStrictness id)
-- TODO: Not sure if that special handling of join points is really
-- necessary. It might even be harmful if the excess 'args' aren't unboxed
-- and we blindly assume that they have the CPR property! So we should
-- try out getting rid of this special case and 'args'.
(rhs_ty, rhs')
- | isJoinId id = cprAnal env (assumed_arg_tys ++ args) rhs
- | otherwise = cprAnal env assumed_arg_tys rhs
+ | isJoinId id = cprAnal env (assumed_arg_cpr_tys ++ args) rhs
+ | otherwise = cprAnal env assumed_arg_cpr_tys rhs
-- possibly trim thunk CPR info
rhs_ty'
@@ -438,86 +453,48 @@ emptyAnalEnv fam_envs
, ae_fam_envs = fam_envs
}
--- | Extend an environment with the strictness IDs attached to the id
+extendAnalEnv :: AnalEnv -> Id -> CprSig -> AnalEnv
+extendAnalEnv env id sig
+ = env { ae_sigs = extendVarEnv (ae_sigs env) id sig }
+
+extendAnalEnvList :: AnalEnv -> [(Id, CprSig)] -> AnalEnv
+extendAnalEnvList env ids_cprs
+ = env { ae_sigs = extendVarEnvList (ae_sigs env) ids_cprs }
+
+-- | Extend an environment with the CPR signatures attached to the id
extendAnalEnvs :: AnalEnv -> [Id] -> AnalEnv
extendAnalEnvs env ids
= env { ae_sigs = sigs' }
where
sigs' = extendVarEnvList (ae_sigs env) [ (id, idCprInfo id) | id <- ids ]
-extendAnalEnv :: AnalEnv -> Id -> CprSig -> AnalEnv
-extendAnalEnv env id sig
- = env { ae_sigs = extendVarEnv (ae_sigs env) id sig }
-
lookupSigEnv :: AnalEnv -> Id -> Maybe CprSig
lookupSigEnv env id = lookupVarEnv (ae_sigs env) id
nonVirgin :: AnalEnv -> AnalEnv
nonVirgin env = env { ae_virgin = False }
-dummyArgs :: DataCon -> [CprType]
-dummyArgs dc = take (dataConRepArity dc) (repeat topCprType)
-
--- | A version of 'extendAnalEnv' for a binder of which we don't see the RHS
--- needed to compute a 'CprSig' (e.g. lambdas and DataAlt field binders).
--- In this case, we can still look at their demand to attach CPR signatures
--- anticipating the unboxing done by worker/wrapper.
--- See Note [CPR for binders that will be unboxed].
-extendAnalEnvForDemand :: AnalEnv -> Id -> Demand -> CprType -> AnalEnv
-extendAnalEnvForDemand env id dmd ty
- | isId id
- , Just (_, DataConAppContext { dcac_dc = dc })
- <- wantToUnbox (ae_fam_envs env) has_inlineable_prag (idType id) dmd
- -- TODO: Make this deep, depending on the StrDmd
- = extendAnalEnv env id $ CprSig $
- markConCprType Terminates (dataConTag dc) (dummyArgs dc) $
- -- TODO: There has to be a better way of forcing
- snd $ forceCprTy (getStrDmd dmd) ty
- | otherwise
- = env
- where
- -- Rather than maintaining in AnalEnv whether we are in an INLINEABLE
- -- function, we just assume that we aren't. That flag is only relevant
- -- to Note [Do not unpack class dictionaries], the few unboxing
- -- opportunities on dicts it prohibits are probably irrelevant to CPR.
- has_inlineable_prag = False
-
-extendEnvForDataAlt :: AnalEnv -> CoreExpr -> Id -> CprType -> DataCon -> [Var] -> AnalEnv
+extendEnvForDataAlt :: AnalEnv -> Id -> CprType -> DataCon -> [Var] -> AnalEnv
-- See Note [CPR in a DataAlt case alternative]
-extendEnvForDataAlt env scrut case_bndr case_bndr_ty dc bndrs
- = foldl' do_con_arg env' ids_w_strs
+extendEnvForDataAlt env case_bndr case_bndr_ty dc bndrs
+ = extendAnalEnv env' case_bndr (CprSig case_bndr_ty')
where
- env' = extendAnalEnv env case_bndr (CprSig case_bndr_sig)
-
- ids_w_strs = filter isId bndrs `zip` dataConRepStrictness dc
-
tycon = dataConTyCon dc
is_product = isJust (isDataProductTyCon_maybe tycon)
is_sum = isJust (isDataSumTyCon_maybe tycon)
- case_bndr_sig
- | is_product || is_sum = undefined -- markConCprType (dataConTag dc) (dummyArgs dc) case_bndr_ty
+ case_bndr_ty'
+ | is_product || is_sum = markConCprType dc case_bndr_ty
-- Any of the constructors had existentials. This is a little too
-- conservative (after all, we only care about the particular data con),
-- but there is no easy way to write is_sum and this won't happen much.
- | otherwise = case_bndr_ty
-
- -- We could have much deeper CPR info here with Nested CPR, which could
- -- propagate available unboxed things from the scrutinee, getting rid of
- -- the is_var_scrut heuristic. See Note [CPR in a DataAlt case alternative].
- -- Giving strict binders the CPR property only makes sense for products, as
- -- the arguments in Note [CPR for binders that will be unboxed] don't apply
- -- to sums (yet); we lack WW for strict binders of sum type.
- do_con_arg env (id, str)
- | is_var scrut
- -- See Note [Add demands for strict constructors] in WorkWrap.Lib
- , let dmd = applyWhen (isMarkedStrict str) strictifyDmd (idDemandInfo id)
- = extendAnalEnvForDemand env id dmd topCprType
- | otherwise
- = env
-
- is_var (Cast e _) = is_var e
- is_var (Var v) = isLocalId v
- is_var _ = False
+ | otherwise = case_bndr_ty
+ env'
+ | Just fields <- splitConCprTy dc case_bndr_ty'
+ , let ids = filter isId bndrs
+ , let cpr_tys = map (CprSig . CprType 0) fields
+ = extendAnalEnvList env (zipEqual "extendEnvForDataAlt" ids cpr_tys)
+ | otherwise
+ = env
{- Note [Ensuring termination of fixed-point iteration]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
=====================================
compiler/GHC/Types/Cpr.hs
=====================================
@@ -10,11 +10,11 @@
module GHC.Types.Cpr (
TerminationFlag (Terminates),
Cpr, topCpr, botCpr, conCpr, initRecFunCpr, lubCpr, asConCpr,
- CprType (..), topCprType, botCprType, conCprType, pruneDeepCpr,
- markConCprType, lubCprType, applyCprTy, abstractCprTy, abstractCprTyNTimes,
- ensureCprTyArity, trimCprTy,
+ CprType (..), topCprType, botCprType, lubCprType, conCprType,
+ pruneDeepCpr, markConCprType, splitConCprTy, applyCprTy, abstractCprTy,
+ abstractCprTyNTimes, ensureCprTyArity, trimCprTy,
forceCprTy, forceCpr, bothCprType,
- cprTransformDataConSig, cprTransformSig, argCprTypesFromStrictSig,
+ cprTransformDataConSig, UnboxingStrategy, cprTransformSig, argCprTypesFromStrictSig,
CprSig (..), mkCprSig, mkCprSigForArity,
topCprSig, seqCprSig
) where
@@ -26,6 +26,7 @@ import GhcPrelude
import GHC.Types.Basic
import GHC.Types.Demand
import GHC.Core.DataCon
+import GHC.Core.Type
import Outputable
import Binary
import Util
@@ -179,7 +180,7 @@ initRecFunCpr :: Cpr
initRecFunCpr = Cpr MightDiverge Bot
conCpr :: TerminationFlag -> ConTag -> [Cpr] -> Cpr
-conCpr tm t fs = Cpr tm (Levitate (Con t fs))
+conCpr tf t fs = Cpr tf (Levitate (Con t fs))
-- | Forget encoded CPR info, but keep termination info.
forgetCpr :: Cpr -> Termination
@@ -244,6 +245,18 @@ topCprType = CprType 0 topCpr
botCprType :: CprType
botCprType = CprType 0 botCpr
+lubCprType :: CprType -> CprType -> CprType
+lubCprType ty1@(CprType n1 cpr1) ty2@(CprType n2 cpr2)
+ | ct_cpr ty1 == botCpr && n1 <= n2 = ty2
+ | ct_cpr ty2 == botCpr && n2 <= n1 = ty1
+ -- There might be non-bottom CPR types with mismatching arities.
+ -- Consider test DmdAnalGADTs. We want to return topCpr in these cases.
+ -- Returning topCprType is a safe default.
+ | n1 == n2
+ = CprType n1 (lubCpr cpr1 cpr2)
+ | otherwise
+ = topCprType
+
extractArgCprAndTermination :: [CprType] -> [Cpr]
extractArgCprAndTermination = map go
where
@@ -256,22 +269,30 @@ conCprType con_tag args = CprType 0 (conCpr Terminates con_tag cprs)
where
cprs = extractArgCprAndTermination args
-markConCprType :: TerminationFlag -> ConTag -> [CprType] -> CprType -> CprType
-markConCprType tf con_tag args ty = ASSERT( ct_arty ty == 0 ) ty { ct_cpr = conCpr tf con_tag cprs }
+markConCprType :: DataCon -> CprType -> CprType
+markConCprType dc _ty@(CprType n cpr)
+ = ASSERT2( n == 0, ppr _ty ) CprType 0 (conCpr Terminates con_tag fields)
where
- cprs = extractArgCprAndTermination args
-
-lubCprType :: CprType -> CprType -> CprType
-lubCprType ty1@(CprType n1 cpr1) ty2@(CprType n2 cpr2)
- | ct_cpr ty1 == botCpr && n1 <= n2 = ty2
- | ct_cpr ty2 == botCpr && n2 <= n1 = ty1
- -- There might be non-bottom CPR types with mismatching arities.
- -- Consider test DmdAnalGADTs. We want to return topCpr in these cases.
- -- Returning topCprType is a safe default.
- | n1 == n2
- = CprType n1 (lubCpr cpr1 cpr2)
- | otherwise
- = topCprType
+ con_tag = dataConTag dc
+ wkr_arity = dataConRepArity dc
+ fields = case cpr of
+ NoMoreCpr (Term _ (Levitate (Con t terms)))
+ | con_tag == t -> map NoMoreCpr terms
+ NoMoreCpr (Term _ Bot) -> replicate wkr_arity (NoMoreCpr botTerm)
+ Cpr _ (Levitate (Con t cprs))
+ | con_tag == t -> cprs
+ Cpr _ Bot -> replicate wkr_arity botCpr
+ _ -> replicate wkr_arity topCpr
+
+splitConCprTy :: DataCon -> CprType -> Maybe [Cpr]
+splitConCprTy dc (CprType 0 (Cpr _ l))
+ | Bot <- l
+ = Just (replicate (dataConRepArity dc) botCpr)
+ | Levitate (Con t fields) <- l
+ , dataConTag dc == t
+ = Just fields
+splitConCprTy _ _
+ = Nothing
applyCprTy :: CprType -> CprType
applyCprTy (CprType n cpr)
@@ -392,18 +413,15 @@ forceCpr str cpr = runTerminationM (forceCprM str cpr)
-- | 'lubTerm's the given outer @TerminationFlag@ on the @CprType at s 'ct_term'.
bothCprType :: CprType -> TerminationFlag -> CprType
--- deepTerm because we only want to affect the WHNF layer.
--- If tm = Terminates, it's just 'id'.
--- If tm = MightDiverge, it will only set the WHNF layer to MightDiverge,
+-- If tf = Terminates, it's just 'id'.
+-- If tf = MightDiverge, it will only set the WHNF layer to MightDiverge,
-- leaving nested termination info (e.g. on product components) intact.
-bothCprType ct tf = ct { ct_cpr = bothCpr (ct_cpr ct) tf }
-
-bothCpr :: Cpr -> TerminationFlag -> Cpr
-bothCpr (NoMoreCpr t) tf = NoMoreCpr (bothTerm t tf)
-bothCpr (Cpr tf1 l_sh) tf2 = Cpr (lubTermFlag tf1 tf2) l_sh
+bothCprType ct Terminates = ct
+bothCprType ct MightDiverge = ct { ct_cpr = shallowDivCpr (ct_cpr ct) }
-bothTerm :: Termination -> TerminationFlag -> Termination
-bothTerm (Term tf1 l_sh) tf2 = Term (lubTermFlag tf1 tf2) l_sh
+shallowDivCpr :: Cpr -> Cpr
+shallowDivCpr (NoMoreCpr (Term _ l_sh)) = NoMoreCpr (Term MightDiverge l_sh)
+shallowDivCpr (Cpr _ l_sh) = Cpr MightDiverge l_sh
seqCprType :: CprType -> ()
seqCprType (CprType _ cpr) = seqCpr cpr
@@ -461,20 +479,24 @@ cprTransformDataConSig con args
cprTransformSig :: StrictSig -> CprSig -> [CprType] -> CprType
cprTransformSig str_sig (CprSig sig_ty) arg_tys
- | arg_strs <- argStrsFromStrictSig str_sig
- , arg_tys `equalLength` arg_strs
- , ASSERT( length arg_tys == ct_arty sig_ty ) True
- , (tf, _) <- runTerminationM $ zipWithM_ forceCprTyM arg_strs arg_tys
+ | arg_strs <- map getStrDmd $ argDmdsFromStrictSig str_sig
+ , arg_strs `leLength` arg_tys
+ , arg_tys `lengthIs` ct_arty sig_ty
+ -- Maybe we should use resTypeArgDmd instead of strTop here. On the other
+ -- hand, I don't think it makes much of a difference; We basically only need
+ -- to pad with strTop when str_sig was topSig to begin with.
+ , (tf, _) <- runTerminationM $ zipWithM_ forceCprTyM (arg_strs ++ repeat strTop) arg_tys
= sig_ty `bothCprType` tf
- -- TODO: Think about what happens if arg_tys is longer than arg_strs.
| otherwise
= topCprType
-- | We have to be sure that 'cprTransformSig' and 'argCprTypesFromStrictSig'
--- agree in how they compute the 'ArgStr's for which the 'CprSig' is computed.
--- This function encodes the common logic.
-argStrsFromStrictSig :: StrictSig -> [ArgStr]
-argStrsFromStrictSig = map getStrDmd . fst . splitStrictSig
+-- agree in how they compute the 'Demand's for which the 'CprSig' is computed.
+-- This function encodes the common (trivial) logic.
+argDmdsFromStrictSig :: StrictSig -> [Demand]
+argDmdsFromStrictSig = fst . splitStrictSig
+
+type UnboxingStrategy = Type -> Demand -> Maybe (DataCon, [Type], [Demand])
-- | Produces 'CprType's the termination info of which match the given
-- strictness signature. Examples:
@@ -482,11 +504,15 @@ argStrsFromStrictSig = map getStrDmd . fst . splitStrictSig
-- - A head-strict demand @S@ would translate to @#@, a
-- - A tuple demand @S(S,L)@ would translate to @#(#,*)@
-- - A call demand @C(S)@ would translate to @strTop -> #(#,*)@
-argCprTypesFromStrictSig :: StrictSig -> [CprType]
-argCprTypesFromStrictSig sig = arg_tys
+argCprTypesFromStrictSig :: UnboxingStrategy -> [Type] -> StrictSig -> [CprType]
+argCprTypesFromStrictSig want_to_unbox arg_tys sig
+ = zipWith go arg_tys (argDmdsFromStrictSig sig)
where
- arg_strs = argStrsFromStrictSig sig
- arg_tys = zipWith ((snd .) . forceCprTy) arg_strs (repeat topCprType)
+ go arg_ty arg_dmd
+ | Just (dc, arg_tys, arg_dmds) <- want_to_unbox arg_ty arg_dmd
+ = conCprType (dataConTag dc) (zipWith go arg_tys arg_dmds)
+ | otherwise
+ = snd $ forceCprTy (getStrDmd arg_dmd) topCprType
---------------
-- * Outputable
@@ -547,8 +573,8 @@ instance Binary r => Binary (KnownShape r) where
get bh = Con <$> get bh <*> get bh
instance Binary TerminationFlag where
- put_ bh MightDiverge = put_ bh False
put_ bh Terminates = put_ bh True
+ put_ bh MightDiverge = put_ bh False
get bh = do
b <- get bh
if b
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/063b38a6a8575219f0a4d7e8d5eab2a58060b01d
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/063b38a6a8575219f0a4d7e8d5eab2a58060b01d
You're receiving this email because of your account on gitlab.haskell.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/ghc-commits/attachments/20200421/cb20706c/attachment-0001.html>
More information about the ghc-commits
mailing list