[Git][ghc/ghc][wip/abs-den] 2 commits: Write a denotationaler interpreter for Core
Sebastian Graf (@sgraf812)
gitlab at gitlab.haskell.org
Tue Jan 9 11:32:57 UTC 2024
Sebastian Graf pushed to branch wip/abs-den at Glasgow Haskell Compiler / GHC
Commits:
93d66e62 by Sebastian Graf at 2024-01-09T12:32:39+01:00
Write a denotationaler interpreter for Core
- - - - -
5e864c67 by Sebastian Graf at 2024-01-09T12:32:39+01:00
Retrofit DmdAnal
- - - - -
6 changed files:
- compiler/GHC/Builtin/Uniques.hs
- compiler/GHC/Core/Opt/DmdAnal.hs
- + compiler/GHC/Core/Semantics.hs
- compiler/GHC/Data/STuple.hs
- compiler/ghc.cabal.in
- + ghdi.hs
Changes:
=====================================
compiler/GHC/Builtin/Uniques.hs
=====================================
@@ -51,6 +51,9 @@ module GHC.Builtin.Uniques
-- Boxing data types
, mkBoxingTyConUnique, boxingDataConUnique
+ -- Denotational interpreter 'GHC.Core.Semantics.eval'
+ , mkTempDataConArgUnique
+
) where
import GHC.Prelude
@@ -326,7 +329,8 @@ Allocation of unique supply characters:
j constraint tuple superclass selectors
k constraint tuple tycons
m constraint tuple datacons
- n Native/LLVM codegen
+ n Native/LLVM codegen, as well as GHC.Core.Semantics / demand analysis
+ (NB: The lifetimes of those uniques do not overlap)
r Hsc name cache
s simplifier
u Cmm pipeline
@@ -445,3 +449,8 @@ mkBoxingTyConUnique i = mkUniqueInt 'b' (5*i)
boxingDataConUnique :: Unique -> Unique
boxingDataConUnique u = stepUnique u 2
+
+-- | Make a temporary unique for a DataCon worker PAP, where we know exactly the
+-- scope of said unique. Used in 'GHC.Core.Semantics.eval'.
+mkTempDataConArgUnique :: Int -> Unique
+mkTempDataConArgUnique i = mkUniqueInt 'n' i
=====================================
compiler/GHC/Core/Opt/DmdAnal.hs
=====================================
@@ -7,6 +7,9 @@
-----------------
-}
{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
module GHC.Core.Opt.DmdAnal
@@ -33,6 +36,7 @@ import GHC.Core.Multiplicity ( scaledThing )
import GHC.Core.FamInstEnv
import GHC.Core.Opt.Arity ( typeArity )
import GHC.Core.Opt.WorkWrap.Utils
+import GHC.Core.Semantics
import GHC.Builtin.PrimOps
import GHC.Builtin.Types.Prim ( realWorldStatePrimTy )
@@ -60,6 +64,8 @@ import Control.Monad.Trans.Reader
import Control.Monad (zipWithM_)
import GHC.Data.Maybe
import Data.Foldable (foldlM)
+import qualified Data.Semigroup as Semi
+import Data.Coerce
{-
************************************************************************
@@ -2704,3 +2710,43 @@ annotateProgram anns = runIdentity . traverseBinders (Identity . annotate)
= bndr `setIdDemandInfo` dmd
| otherwise
= bndr
+
+-- Semantics stuff
+newtype LexicalEnv = LE { le_how_bound :: NameEnv TopLevelFlag }
+
+newtype PlusDmdEnv = PDE DmdEnv
+instance Semi.Semigroup PlusDmdEnv where
+ (<>) = coerce plusDmdEnv
+instance Monoid PlusDmdEnv where
+ mempty = PDE nopDmdEnv
+
+newtype DmdT v = DmdT { unDmdT :: LexicalEnv -> SubDemand -> SPair v DmdEnv }
+ deriving (Functor,Applicative,Monad) via (ReaderT LexicalEnv (ReaderT SubDemand (SWriter PlusDmdEnv)))
+
+type DmdVal = [Demand]
+ -- Think
+ -- data DmdVal = DmdFun Demand DmdVal | DmdNop
+ -- NB: lacks constructor values; these are always DmdNop
+
+type DmdD = DmdT DmdVal
+ -- Think: demand transformer, SubDemand -> DmdType
+
+dmdD2DmdType :: DmdD -> LexicalEnv -> SubDemand -> DmdType
+dmdD2DmdType d le sd = case unDmdT d le sd of S2 val env -> DmdType env val
+dmdType2DmdD :: (LexicalEnv -> SubDemand -> DmdType) -> DmdD
+dmdType2DmdD trans = DmdT $ \le sd -> case trans le sd of DmdType env val -> S2 val env
+
+instance Trace DmdD where
+ step (Lookup x) d = DmdT $ \le sd -> case (unDmdT d le sd, lookupNameEnv (le_how_bound le) x) of
+ (S2 val env, Just NotTopLevel) -> S2 val (addVarDmdEnv x (C_11 :* sd) env)
+ (S2 val env, Just TopLevel)
+ | isInterestingTopLevelFn var
+ -- Top-level things will be used multiple times or not at
+ -- all anyway, hence the multDmd below: It means we don't
+ -- have to track whether @var@ is used strictly or at most
+ -- once, because ultimately it never will.
+ -> S2 val (addVarDmdEnv x (C_0N `multDmd` (C_11 :* sd))) -- discard strictness
+ -- not interesting: fall through, don't bother tracking;
+ -- just annotate with 'topDmd' at bindings site
+ (t, _) -> t -- GlobalId or local, top-level and not interesting
+ step _ d = d
=====================================
compiler/GHC/Core/Semantics.hs
=====================================
@@ -0,0 +1,247 @@
+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
+-- {-# OPTIONS_GHC -fdefer-type-errors #-}
+-- {-# OPTIONS_GHC -Wwarn #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE QuantifiedConstraints #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module GHC.Core.Semantics where
+
+import GHC.Prelude
+
+import GHC.Builtin.Names
+import GHC.Builtin.Uniques
+
+import GHC.Core
+import GHC.Core.Coercion
+import GHC.Core.DataCon
+
+import qualified GHC.Data.Word64Map as WM
+
+import GHC.Types.Literal
+import GHC.Types.Id
+import GHC.Types.Name
+import GHC.Types.Var.Env
+import GHC.Types.Unique.FM
+import GHC.Types.Unique.Set
+
+import GHC.Utils.Misc
+import GHC.Utils.Outputable
+
+import Control.Monad
+import Control.Monad.Trans.State
+import Data.Word
+import GHC.Core.Utils hiding (findAlt)
+import GHC.Core.Type
+import GHC.Builtin.PrimOps
+
+data Event = Lookup Name | LookupArg CoreExpr | Update | App1 | App2 | Case1 | Case2 | Let1
+
+class Trace d where
+ step :: Event -> d -> d
+
+-- A slight extension of the Domain type from the paper.
+-- Note that the 'Name's bear no semantic significance: The `Domain (D τ)`
+-- instance simply ignores them. They are useful for analyses, however.
+class Domain d where
+ stuck :: d
+ erased :: d -- Think of it like coercionToken#
+ lit :: Literal -> d
+ primOp :: PrimOp -> d
+ fun :: Id -> Type -> (d -> d) -> d
+ con :: DataCon -> [d] -> d
+ apply :: d -> d -> d
+ select :: d -> Id -> [DAlt d] -> d
+type DAlt d = (AltCon, [Id], d -> [d] -> d)
+
+data BindHint = BindArg | BindNonRec Name | BindRec [Name]
+class HasBind d where
+ bind :: BindHint -> [[d] -> d] -> ([d] -> d) -> d
+ -- NB: The `BindHint` bears no semantic sigificance:
+ -- `HasBind (D (ByNeed T))` does not look at it.
+ -- Still useful for analyses!
+
+seq_ :: Domain d => d -> d -> d
+seq_ a b = select a wildCardName [(DEFAULT, [], \_a _ds -> b)]
+
+anfise :: (Trace d, Domain d, HasBind d) => CoreExpr -> IdEnv d -> (d -> d) -> d
+anfise (Lit l) _ k = k (lit l)
+anfise (Var x) env k | Just d <- lookupVarEnv env x = k d
+ | otherwise = stuck
+anfise (Coercion co) env k = -- co is unlifted and will evaluate to coercionToken#
+ foldr (\x -> seq_ (eval (Var x) env)) (k erased) (NonDetUniqFM $ getUniqSet $ coVarsOfCo co)
+anfise (Type _ty) _ k = k erased
+anfise (Tick _t e) env k = anfise e env k
+anfise (Cast e _co) env k = anfise e env k
+anfise e env k = bind BindArg [const (step (LookupArg e) (eval e env))]
+ (\ds -> if isUnliftedType (exprType e)
+ then only ds `seq_` k (only ds)
+ else k (only ds))
+
+anfiseMany :: (Trace d, Domain d, HasBind d) => [CoreExpr] -> IdEnv d -> ([d] -> d) -> d
+anfiseMany es env k = go es []
+ where
+ go [] ds = k (reverse ds)
+ go (e:es) ds = anfise e env $ \d -> go es (d:ds)
+
+eval :: (Trace d, Domain d, HasBind d) => CoreExpr -> IdEnv d -> d
+eval (Coercion co) env = anfise (Coercion co) env id
+eval (Type _ty) _ = erased
+eval (Lit l) _ = lit l
+eval (Tick _t e) env = eval e env
+eval (Cast e _co) env = eval e env
+eval (Var x) env
+ | Just dc <- isDataConWorkId_maybe x = con dc [] -- TODO
+ | Just op <- isPrimOpId_maybe x = primOp op
+ | isDataConWrapId x = eval (unfoldingTemplate (idUnfolding x)) emptyVarEnv
+ | Just d <- lookupVarEnv env x = d
+ | otherwise = stuck -- Scoping error. Actually ruled out by the Core type system
+eval (Lam x e) env = fun (idName x) (\d -> step App2 (eval e (extendVarEnv env x d)))
+eval e at App{} env
+ | Var v <- f, Just dc <- isDataConWorkId_maybe v
+ = anfiseMany as env $ \ds -> case compare (dataConRepArity dc) (length ds) of
+ EQ -> con dc ds
+ GT -> stuck -- oversaturated => stuck
+ LT -> expand [] (take (length ds - dataConRepArity dc) papNames) -- undersaturated => PAP
+ where
+ expand etas [] = con dc (ds ++ reverse etas)
+ expand etas (x:xs) = fun x (\d -> expand (d:etas) xs)
+ | otherwise
+ = go (eval f env) as
+ where
+ (f, as) = collectArgs e
+ go df [] = df
+ go df (a:as) = go (anfise a env (step App1 . apply df)) as
+eval (Let (NonRec x rhs) body) env =
+ bind (BindNonRec (idName x))
+ [const (step (Lookup (idName x)) (eval rhs env))]
+ (\ds -> step Let1 (eval body (extendVarEnv env x (only ds))))
+eval (Let (Rec binds) body) env =
+ bind (BindRec (map idName xs))
+ [\ds -> step (Lookup (idName x)) (eval rhs (new_env ds)) | (x,rhs) <- binds]
+ (\ds -> step Let1 (eval body (new_env ds)))
+ where
+ xs = map fst binds
+ new_env ds = extendVarEnvList env (zip xs ds)
+eval (Case e b _ty alts) env = step Case1 $
+ select (eval e env) (idName b)
+ [ (con, map idName xs, cont xs rhs) | Alt con xs rhs <- alts ]
+ where
+ cont xs rhs scrut ds = step Case2 $ eval rhs (extendVarEnvList env (zipEqual "eval Case{}" (b:xs) (scrut:ds)))
+
+x1,x2 :: Name
+papNames :: [Name]
+papNames@(x1:x2:_) = [ mkSystemName (mkTempDataConArgUnique i) (mkVarOcc "pap") | i <- [0..] ]
+
+
+-- By-need semantics, from the paper
+
+data T v = Step Event (T v) | Ret v
+ deriving Functor
+instance Applicative T where pure = Ret; (<*>) = ap
+instance Monad T where Ret a >>= f = f a; Step ev t >>= f = Step ev (t >>= f)
+instance Trace (T v) where step = Step
+
+type D τ = τ (Value τ)
+data Value τ
+ = Stuck
+ | Erased
+ | Litt Literal
+ | Fun (D τ -> D τ)
+ | Con DataCon [D τ]
+
+instance (Trace (D τ), Monad τ) => Domain (D τ) where
+ stuck = return Stuck
+ erased = return Erased
+ lit l = return (Litt l)
+ fun _x f = return (Fun f)
+ con k ds = return (Con k ds)
+ apply d a = d >>= \case Fun f -> f a; _ -> stuck
+ select d _b fs = d >>= \v -> case v of
+ Stuck -> stuck
+ Con k ds | Just (_con, _xs, f) <- findAlt (DataAlt k) fs -> f (return v) ds
+ Litt l | Just (_con, _xs, f) <- findAlt (LitAlt l) fs -> f (return v) []
+ _ | Just (_con, _xs, f) <- findAlt DEFAULT fs -> f (return v) []
+ _ -> stuck
+ primOp op = case op of
+ IntAddOp -> intop (+)
+ IntMulOp -> intop (*)
+ IntRemOp -> intop rem
+ _ -> stuck
+ where
+ intop op = binop (\v1 v2 -> case (v1,v2) of (Litt (LitNumber LitNumInt i1), Litt (LitNumber LitNumInt i2)) -> Litt (LitNumber LitNumInt (i1 `op` i2)); _ -> Stuck)
+ binop f = fun x1 $ \d1 -> step App2 $ fun x2 $ \d2 -> step App2 $ f <$> d1 <*> d2
+
+-- The following function was copy and pasted from GHC.Core.Utils.findAlt:
+findAlt :: AltCon -> [DAlt d] -> Maybe (DAlt d)
+ -- A "Nothing" result *is* legitimate
+ -- See Note [Unreachable code]
+findAlt con alts
+ = case alts of
+ (deflt@(DEFAULT, _, _):alts) -> go alts (Just deflt)
+ _ -> go alts Nothing
+ where
+ go [] deflt = deflt
+ go (alt@(con1, _, _) : alts) deflt
+ = case con `cmpAltCon` con1 of
+ LT -> deflt -- Missed it already; the alts are in increasing order
+ EQ -> Just alt
+ GT -> go alts deflt
+
+-- By-need semantics, straight from the paper
+
+type Addr = Word64
+type Heap τ = WM.Word64Map (D τ)
+newtype ByNeed τ v = ByNeed { runByNeed :: StateT (Heap (ByNeed τ)) τ v }
+ deriving (Functor, Applicative, Monad)
+
+instance (forall v. Trace (τ v)) => Trace (ByNeed τ v) where
+ step ev (ByNeed (StateT m)) = ByNeed $ StateT $ step ev . m
+
+fetch :: Monad τ => Addr -> D (ByNeed τ)
+fetch a = ByNeed get >>= \μ -> μ WM.! a
+
+memo :: forall τ. (Monad τ, forall v. Trace (τ v)) => Addr -> D (ByNeed τ) -> D (ByNeed τ)
+memo a d = d >>= ByNeed . StateT . upd
+ where upd Stuck μ = return (Stuck :: Value (ByNeed τ), μ)
+ upd v μ = step Update (return (v, WM.insert a (memo a (return v)) μ))
+
+freeList :: Heap τ -> [Addr]
+freeList μ = [a..]
+ where a = case WM.lookupMax μ of Just (a,_) -> a+1; _ -> 0
+
+instance (Monad τ, forall v. Trace (τ v)) => HasBind (D (ByNeed τ)) where
+ bind _hint rhss body = do
+ as <- take (length rhss) . freeList <$> ByNeed get
+ let ds = map fetch as
+ ByNeed $ modify (\μ -> foldr (\(a,rhs) -> WM.insert a (memo a (rhs ds))) μ (zip as rhss))
+ body ds
+
+evalByNeed :: CoreExpr -> T (Value (ByNeed T), Heap (ByNeed T))
+evalByNeed e = runStateT (runByNeed (eval e emptyVarEnv)) WM.empty
+
+-- Boilerplate
+instance Outputable Event where
+ ppr (Lookup n) = text "Lookup" <> parens (ppr n)
+ ppr (LookupArg e) = text "LookupArg" <> parens (ppr e)
+ ppr Update = text "Update"
+ ppr App1 = text "App1"
+ ppr App2 = text "App2"
+ ppr Case1 = text "Case1"
+ ppr Case2 = text "Case2"
+ ppr Let1 = text "Let1"
+instance Outputable v => Outputable (T v) where
+ ppr (Step ev τ) = ppr ev <> arrow <> ppr τ
+ ppr (Ret v) = char '<' <> ppr v <> char '>'
+instance Outputable (Value τ) where
+ ppr Stuck = text "stuck"
+ ppr Erased = char '_'
+ ppr (Litt l) = ppr l
+ ppr (Fun _f) = text "Fun"
+ ppr (Con dc _ds) = ppr dc
+instance Outputable (Heap τ) where
+ ppr μ = brackets (pprWithCommas (\(a,_) -> ppr a <> char '↦' <> underscore) (WM.toList μ))
=====================================
compiler/GHC/Data/STuple.hs
=====================================
@@ -2,10 +2,12 @@
module GHC.Data.STuple
( SPair(..), swap, toPair, sFirst, sSecond, sUnzip
, STriple(..), mapSSndOf3, mapSTrdOf3, toTriple
- , SQuad(..), toQuad
+ , SWriter, runSWriter, SWriterT(..), runSWriterT, sTell
) where
import GHC.Prelude
+import Data.Functor.Identity
+import qualified Data.Semigroup as Semigroup
-- | Strict pair data type
data SPair a b = S2 { sFst :: !a, sSnd :: !b }
@@ -37,8 +39,29 @@ mapSTrdOf3 f (S3 a b c) = S3 a b (f c)
toTriple :: STriple a b c -> (a, b, c)
toTriple (S3 a b c) = (a, b, c)
--- | Strict quadruple data type
-data SQuad a b c d = S4 { sFstOf4 :: !a, sSndOf4 :: !b, sTrdOf4 :: !c, sFthOf4 :: !d }
+-- | Strict 'Writer' monad
+type SWriter w = SWriterT w Identity
-toQuad :: SQuad a b c d -> (a, b, c, d)
-toQuad (S4 a b c d) = (a, b, c, d)
+runSWriter :: Monoid w => SWriter w a -> SPair a w
+runSWriter = runIdentity . runSWriterT
+
+-- | Strict 'Writer' monad
+newtype SWriterT w m a = SWriterT { unSWriterT :: m (SPair a w) }
+
+runSWriterT :: Monoid w => SWriterT w m a -> m (SPair a w)
+runSWriterT = unSWriterT
+
+instance Functor f => Functor (SWriterT w f) where
+ fmap f = SWriterT . fmap (\(S2 a w) -> S2 (f a) w) . unSWriterT
+instance (Monoid w, Applicative f) => Applicative (SWriterT w f) where
+ pure a = SWriterT $ pure (S2 a mempty)
+ SWriterT f <*> SWriterT a = SWriterT (g <$> f <*> a)
+ where g (S2 f w1) (S2 a w2) = S2 (f a) (w1 Semigroup.<> w2)
+instance (Monoid w, Monad m) => Monad (SWriterT w m) where
+ SWriterT m >>= k = SWriterT $ do
+ S2 a w1 <- m
+ S2 b w2 <- unSWriterT (k a)
+ pure $! S2 b (w1 Semigroup.<> w2)
+
+sTell :: Applicative f => w -> SWriterT w f ()
+sTell w = SWriterT $ pure $! S2 () w
=====================================
compiler/ghc.cabal.in
=====================================
@@ -374,6 +374,7 @@ Library
GHC.Core.Opt.WorkWrap.Utils
GHC.Core.PatSyn
GHC.Core.Ppr
+ GHC.Core.Semantics
GHC.Types.TyThing.Ppr
GHC.Core.Predicate
GHC.Core.Reduction
=====================================
ghdi.hs
=====================================
@@ -0,0 +1,84 @@
+-- Import necessary modules
+import GHC
+import GHC.Driver.Config.Parser
+import GHC.Driver.Env.Types
+import GHC.Driver.Session
+import GHC.Utils.Outputable
+import GHC.Unit.Types
+import GHC.Unit.Module.ModGuts
+import GHC.Data.StringBuffer
+import GHC.Data.FastString
+import qualified GHC.Parser.Lexer as L
+import qualified GHC.Parser as P
+import GHC.Types.SrcLoc
+import GHC.Core
+import Control.Monad
+import Control.Monad.IO.Class
+import System.IO
+import System.Environment
+import System.Exit
+import System.Directory
+import System.FilePath
+import Data.List
+import GHC.Types.Name
+import GHC.Core.Semantics
+import qualified GHC.LanguageExtensions as LangExt
+
+import System.Console.Haskeline
+
+indent :: Int -> String -> String
+indent n = unlines . map (\s -> replicate n ' ' ++ s) . lines
+
+pprPrint :: Outputable a => a -> IO ()
+pprPrint = putStrLn . showSDocUnsafe . ppr
+
+compileToCore :: String -> [String] -> String -> IO CoreExpr
+compileToCore libdir args expression = do
+ tmp <- getTemporaryDirectory
+ let file = tmp </> "_interactive_.hs"
+ writeFile file ("module Interactive where import GHC.Exts; it = " ++ indent 2 expression)
+ -- Initialize GHC session
+ defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
+ runGhc (Just libdir) $ do
+ -- Set up GHC session
+ dflags <- getSessionDynFlags
+ logger <- getLogger
+ (dflags, rest_args, err_messages) <- parseDynamicFlags logger dflags (map (L noSrcSpan) args)
+ when (not (null rest_args)) $ liftIO $ putStrLn ("Unhandled args: " ++ show rest_args) >> exitFailure
+ when (not (null err_messages)) $ liftIO $ pprPrint err_messages >> exitFailure
+
+ setSessionDynFlags $
+ -- flip gopt_unset Opt_FullLaziness $
+ -- flip gopt_unset Opt_WorkerWrapper $
+ -- updOptLevel 1 $ -- if you want to compile with -O1 opts, make sure to unset -ffull-laziness and -fworker-wrapper above in addition to -flocal-float-out-top-level
+ flip gopt_unset Opt_LocalFloatOutTopLevel $
+ flip gopt_unset Opt_IgnoreInterfacePragmas $ -- This enables cross-module inlining
+ flip xopt_set LangExt.MagicHash $
+ dflags
+ mod_guts <- compileToCoreSimplified file
+ let binds = cm_binds mod_guts
+ let Just (NonRec _ e) = find (\b -> case b of NonRec x e -> getOccString x == "it"; _ -> False) binds
+ return e
+
+-- Main function to handle command-line arguments
+main :: IO ()
+main = do
+ args <- getArgs
+ tmp <- getTemporaryDirectory
+ let settings = defaultSettings { historyFile = Just (tmp </> ".ghdi.hist") }
+ case args of
+ (libdir:rest) -> runInputT settings (loop libdir rest)
+ _ -> putStrLn "Usage: `ghdi <libdir>`, for example `ghdi $(ghc --print-libdir)`"
+
+loop :: FilePath -> [String] -> InputT IO ()
+loop libdir args = do
+ minput <- getInputLine "prompt> "
+ case minput of
+ Nothing -> return ()
+ Just ":quit" -> return ()
+ Just input -> do
+ e <- liftIO $ compileToCore libdir args input
+ outputStrLn (showSDocUnsafe (hang (text "Above expression as (optimised) Core:") 2 (ppr e)))
+ outputStrLn "Trace of denotational interpreter:"
+ outputStrLn (showSDocOneLine defaultSDocContext (hang empty 2 (ppr (evalByNeed e))))
+ loop libdir args
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/827e4170f6b25b7cbd773c87e995407d72572a82...5e864c670c4d7478ffe16e5c3ecfd226a0ceae16
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/827e4170f6b25b7cbd773c87e995407d72572a82...5e864c670c4d7478ffe16e5c3ecfd226a0ceae16
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/20240109/d64b32b5/attachment-0001.html>
More information about the ghc-commits
mailing list