[Git][ghc/ghc][wip/marge_bot_batch_merge_job] 5 commits: Remove Ptr example from roles docs
Marge Bot (@marge-bot)
gitlab at gitlab.haskell.org
Tue Aug 22 13:11:19 UTC 2023
Marge Bot pushed to branch wip/marge_bot_batch_merge_job at Glasgow Haskell Compiler / GHC
Commits:
76a4d11b by Jaro Reinders at 2023-08-22T08:08:13-04:00
Remove Ptr example from roles docs
- - - - -
069729d3 by Bryan Richter at 2023-08-22T08:08:49-04:00
Guard against duplicate pipelines in forks
- - - - -
f861423b by Rune K. Svendsen at 2023-08-22T08:09:35-04:00
dump-decls: fix "Ambiguous module name"-error
Fixes errors of the following kind, which happen when dump-decls is run on a package that contains a module name that clashes with that of another package.
```
dump-decls: <no location info>: error:
Ambiguous module name `System.Console.ANSI.Types':
it was found in multiple packages:
ansi-terminal-0.11.4 ansi-terminal-types-0.11.5
```
- - - - -
cc742a0f by Krzysztof Gogolewski at 2023-08-22T09:11:06-04:00
Fix MultiWayIf linearity checking (#23814)
Co-authored-by: Thomas BAGREL <thomas.bagrel at tweag.io>
- - - - -
d5c835ca by konsumlamm at 2023-08-22T09:11:12-04:00
Update `Control.Concurrent.*` documentation
- - - - -
14 changed files:
- .gitlab-ci.yml
- compiler/GHC/Tc/Gen/Expr.hs
- docs/users_guide/exts/roles.rst
- libraries/base/Control/Concurrent/Chan.hs
- libraries/base/Control/Concurrent/MVar.hs
- libraries/base/Control/Concurrent/QSem.hs
- libraries/base/Control/Concurrent/QSemN.hs
- libraries/base/GHC/MVar.hs
- + testsuite/tests/linear/should_compile/T23814.hs
- testsuite/tests/linear/should_compile/all.T
- + testsuite/tests/linear/should_fail/T23814fail.hs
- + testsuite/tests/linear/should_fail/T23814fail.stderr
- testsuite/tests/linear/should_fail/all.T
- utils/dump-decls/Main.hs
Changes:
=====================================
.gitlab-ci.yml
=====================================
@@ -57,26 +57,45 @@ stages:
# Note [The CI Story]
# ~~~~~~~~~~~~~~~~~~~
#
-# There are two different types of pipelines:
+# There are a few different types of pipelines. Among them:
#
-# - marge-bot merges to `master`. Here we perform an exhaustive validation
+# 1. marge-bot merges to `master`. Here we perform an exhaustive validation
# across all of the platforms which we support. In addition, we push
# performance metric notes upstream, providing a persistent record of the
# performance characteristics of the compiler.
#
-# - merge requests. Here we perform a slightly less exhaustive battery of
+# 2. merge requests. Here we perform a slightly less exhaustive battery of
# testing. Namely we omit some configurations (e.g. the unregisterised job).
# These use the merge request's base commit for performance metric
# comparisons.
#
-
+# These and other pipelines are defined implicitly by the rules of individual
+# jobs.
+#
+# At the top level, however, we can declare that pipelines (of whatever type)
+# only run when:
+#
+# 1. Processing a merge request (as mentioned above)
+#
+# 2. Processing a tag
+#
+# 3. Pushing to master on the root ghc/ghc repo (as mentioned above)
+#
+# 4. Pushing to a release branch on the root ghc/ghc repo
+#
+# 5. Somebody manually triggers a pipeline from the GitLab UI
+#
+# In particular, note that pipelines don't automatically run just when changes
+# are pushed to a feature branch.
workflow:
- # N.B. Don't run on wip/ branches, instead on run on merge requests.
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_TAG
- - if: '$CI_COMMIT_BRANCH == "master"'
- - if: '$CI_COMMIT_BRANCH =~ /ghc-[0-9]+\.[0-9]+/'
+ # N.B.: If we weren't explicit about CI_PROJECT_ID, the following rule would
+ # cause a duplicate pipeline for merge requests coming from the master
+ # branch of a fork.
+ - if: $CI_PROJECT_ID == "1" && $CI_COMMIT_BRANCH == "master"
+ - if: $CI_PROJECT_ID == "1" && $CI_COMMIT_BRANCH =~ /ghc-[0-9]+\.[0-9]+/
- if: '$CI_PIPELINE_SOURCE == "web"'
# which versions of GHC to allow bootstrap with
=====================================
compiler/GHC/Tc/Gen/Expr.hs
=====================================
@@ -396,9 +396,34 @@ tcExpr (HsIf x pred b1 b2) res_ty
; tcEmitBindingUsage (supUE u1 u2)
; return (HsIf x pred' b1' b2') }
+{-
+Note [MultiWayIf linearity checking]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Suppose we'd like to compute the usage environment for
+
+if | b1 -> e1
+ | b2 -> e2
+ | otherwise -> e3
+
+and let u1, u2, v1, v2, v3 denote the usage env for b1, b2, e1, e2, e3
+respectively.
+
+Since a multi-way if is mere sugar for nested if expressions, the usage
+environment should ideally be u1 + sup(v1, u2 + sup(v2, v3)).
+However, currently we don't support linear guards (#19193). All variables
+used in guards from u1 and u2 will have multiplicity Many.
+But in that case, we have equality u1 + sup(x,y) = sup(u1 + x, y),
+ and likewise u2 + sup(x,y) = sup(u2 + x, y) for any x,y.
+Using this identity, we can just compute sup(u1 + v1, u2 + v2, v3) instead.
+This is simple to do, since we get u_i + v_i directly from tcGRHS.
+If we add linear guards, this code will have to be revisited.
+Not using 'sup' caused #23814.
+-}
+
tcExpr (HsMultiIf _ alts) res_ty
- = do { alts' <- mapM (wrapLocMA $ tcGRHS match_ctxt res_ty) alts
+ = do { (ues, alts') <- mapAndUnzipM (\alt -> tcCollectingUsage $ wrapLocMA (tcGRHS match_ctxt res_ty) alt) alts
; res_ty <- readExpType res_ty
+ ; tcEmitBindingUsage (supUEs ues) -- See Note [MultiWayIf linearity checking]
; return (HsMultiIf res_ty alts') }
where match_ctxt = MC { mc_what = IfAlt, mc_body = tcBody }
=====================================
docs/users_guide/exts/roles.rst
=====================================
@@ -155,26 +155,7 @@ Role annotations
Allow role annotation syntax.
Sometimes the programmer wants to constrain the inference process. For
-example, the base library contains the following definition: ::
-
- data Ptr a = Ptr Addr#
-
-The idea is that ``a`` should really be a representational parameter,
-but role inference assigns it to phantom. This makes some level of
-sense: a pointer to an ``Int`` really is representationally the same as
-a pointer to a ``Bool``. But, that's not at all how we want to use
-``Ptr``\ s! So, we want to be able to say ::
-
- type role Ptr representational
- data Ptr a = Ptr Addr#
-
-The ``type role`` (enabled with :extension:`RoleAnnotations`) declaration
-forces the parameter ``a`` to be at role representational, not role
-phantom. GHC then checks the user-supplied roles to make sure they don't
-break any promises. It would be bad, for example, if the user could make
-``BadIdea``\'s role be representational.
-
-As another example, we can consider a type ``Set a`` that represents a
+example, we can consider a type ``Set a`` that represents a
set of data, ordered according to ``a``\'s ``Ord`` instance. While it
would generally be type-safe to consider ``a`` to be at role
representational, it is possible that a ``newtype`` and its base type
=====================================
libraries/base/Control/Concurrent/Chan.hs
=====================================
@@ -13,9 +13,9 @@
--
-- Unbounded channels.
--
--- The channels are implemented with @MVar at s and therefore inherit all the
+-- The channels are implemented with 'Control.Concurrent.MVar's and therefore inherit all the
-- caveats that apply to @MVar at s (possibility of races, deadlocks etc). The
--- stm (software transactional memory) library has a more robust implementation
+-- @stm@ (software transactional memory) library has a more robust implementation
-- of channels called @TChan at s.
--
-----------------------------------------------------------------------------
@@ -43,7 +43,7 @@ import Control.Exception (mask_)
#define _UPK_(x) {-# UNPACK #-} !(x)
-- A channel is represented by two @MVar at s keeping track of the two ends
--- of the channel contents,i.e., the read- and write ends. Empty @MVar at s
+-- of the channel contents, i.e., the read- and write ends. Empty @MVar at s
-- are used to handle consumers trying to read from an empty channel.
-- |'Chan' is an abstract type representing an unbounded FIFO channel.
@@ -59,13 +59,13 @@ data ChItem a = ChItem a _UPK_(Stream a)
-- although it leads to higher allocation, the channel data takes up
-- less space and is therefore quicker to GC.
--- See the Concurrent Haskell paper for a diagram explaining the
+-- See the Concurrent Haskell paper for a diagram explaining
-- how the different channel operations proceed.
-- @newChan@ sets up the read and write end of a channel by initialising
-- these two @MVar at s with an empty @MVar at .
--- |Build and returns a new instance of 'Chan'.
+-- |Build and return a new instance of 'Chan'.
newChan :: IO (Chan a)
newChan = do
hole <- newEmptyMVar
@@ -113,7 +113,7 @@ readChan (Chan readVar _) =
return (new_read_end, val)
-- |Duplicate a 'Chan': the duplicate channel begins empty, but data written to
--- either channel from then on will be available from both. Hence this creates
+-- either channel from then on will be available from both. Hence this creates
-- a kind of broadcast channel, where data written by anyone is seen by
-- everyone else.
--
=====================================
libraries/base/Control/Concurrent/MVar.hs
=====================================
@@ -11,7 +11,7 @@
-- Stability : stable
-- Portability : non-portable (concurrency)
--
--- An @'MVar' t@ is mutable location that is either empty or contains a
+-- An @'MVar' t@ is a mutable location that is either empty or contains a
-- value of type @t at . It has two fundamental operations: 'putMVar'
-- which fills an 'MVar' if it is empty and blocks otherwise, and
-- 'takeMVar' which empties an 'MVar' if it is full and blocks
@@ -25,7 +25,7 @@
-- wait and signal.
--
-- They were introduced in the paper
--- <https://www.haskell.org/ghc/docs/papers/concurrent-haskell.ps.gz "Concurrent Haskell">
+-- ["Concurrent Haskell"](https://www.microsoft.com/en-us/research/wp-content/uploads/1996/01/concurrent-haskell.pdf)
-- by Simon Peyton Jones, Andrew Gordon and Sigbjorn Finne, though
-- some details of their implementation have since then changed (in
-- particular, a put on a full 'MVar' used to error, but now merely
@@ -35,7 +35,7 @@
--
-- 'MVar's offer more flexibility than 'Data.IORef.IORef's, but less flexibility
-- than 'GHC.Conc.STM'. They are appropriate for building synchronization
--- primitives and performing simple interthread communication; however
+-- primitives and performing simple inter-thread communication; however
-- they are very simple and susceptible to race conditions, deadlocks or
-- uncaught exceptions. Do not use them if you need to perform larger
-- atomic operations such as reading from multiple variables: use 'GHC.Conc.STM'
@@ -54,8 +54,8 @@
-- No thread can be blocked indefinitely on an 'MVar' unless another
-- thread holds that 'MVar' indefinitely. One usual implementation of
-- this fairness guarantee is that threads blocked on an 'MVar' are
--- served in a first-in-first-out fashion, but this is not guaranteed
--- in the semantics.
+-- served in a first-in-first-out fashion (this is what GHC does),
+-- but this is not guaranteed in the semantics.
--
-- === Gotchas
--
@@ -64,7 +64,7 @@
-- 'MVar', it will be evaluated by the thread that consumes it, not the
-- thread that produced it. Be sure to 'evaluate' values to be placed
-- in an 'MVar' to the appropriate normal form, or utilize a strict
--- MVar provided by the strict-concurrency package.
+-- @MVar@ provided by the [strict-concurrency](https://hackage.haskell.org/package/strict-concurrency) package.
--
-- === Ordering
--
@@ -89,33 +89,33 @@
-- reader has not read yet, and empty otherwise.
--
-- @
--- data SkipChan a = SkipChan (MVar (a, [MVar ()])) (MVar ())
+-- data SkipChan a = SkipChan (MVar (a, [MVar ()])) (MVar ())
--
--- newSkipChan :: IO (SkipChan a)
--- newSkipChan = do
--- sem <- newEmptyMVar
--- main <- newMVar (undefined, [sem])
--- return (SkipChan main sem)
+-- newSkipChan :: IO (SkipChan a)
+-- newSkipChan = do
+-- sem <- newEmptyMVar
+-- main <- newMVar (undefined, [sem])
+-- return (SkipChan main sem)
--
--- putSkipChan :: SkipChan a -> a -> IO ()
--- putSkipChan (SkipChan main _) v = do
--- (_, sems) <- takeMVar main
--- putMVar main (v, [])
--- mapM_ (\sem -> putMVar sem ()) sems
+-- putSkipChan :: SkipChan a -> a -> IO ()
+-- putSkipChan (SkipChan main _) v = do
+-- (_, sems) <- takeMVar main
+-- putMVar main (v, [])
+-- mapM_ (\\sem -> putMVar sem ()) sems
--
--- getSkipChan :: SkipChan a -> IO a
--- getSkipChan (SkipChan main sem) = do
--- takeMVar sem
--- (v, sems) <- takeMVar main
--- putMVar main (v, sem:sems)
--- return v
+-- getSkipChan :: SkipChan a -> IO a
+-- getSkipChan (SkipChan main sem) = do
+-- takeMVar sem
+-- (v, sems) <- takeMVar main
+-- putMVar main (v, sem : sems)
+-- return v
--
--- dupSkipChan :: SkipChan a -> IO (SkipChan a)
--- dupSkipChan (SkipChan main _) = do
--- sem <- newEmptyMVar
--- (v, sems) <- takeMVar main
--- putMVar main (v, sem:sems)
--- return (SkipChan main sem)
+-- dupSkipChan :: SkipChan a -> IO (SkipChan a)
+-- dupSkipChan (SkipChan main _) = do
+-- sem <- newEmptyMVar
+-- (v, sems) <- takeMVar main
+-- putMVar main (v, sem : sems)
+-- return (SkipChan main sem)
-- @
--
-- This example was adapted from the original Concurrent Haskell paper.
@@ -186,7 +186,7 @@ swapMVar mvar new =
-}
{-# INLINE withMVar #-}
-- inlining has been reported to have dramatic effects; see
--- http://www.haskell.org//pipermail/haskell/2006-May/017907.html
+-- https://mail.haskell.org/pipermail/haskell/2006-May/017907.html
withMVar :: MVar a -> (a -> IO b) -> IO b
withMVar m io =
mask $ \restore -> do
@@ -274,7 +274,7 @@ addMVarFinalizer :: MVar a -> IO () -> IO ()
addMVarFinalizer = GHC.MVar.addMVarFinalizer
-- | Make a 'Weak' pointer to an 'MVar', using the second argument as
--- a finalizer to run when 'MVar' is garbage-collected
+-- a finalizer to run when the 'MVar' is garbage-collected
--
-- @since 4.6.0.0
mkWeakMVar :: MVar a -> IO () -> IO (Weak (MVar a))
=====================================
libraries/base/Control/Concurrent/QSem.hs
=====================================
@@ -34,7 +34,7 @@ import Data.Maybe
--
-- The pattern
--
--- > bracket_ waitQSem signalQSem (...)
+-- > bracket_ waitQSem signalQSem (...)
--
-- is safe; it never loses a unit of the resource.
--
@@ -67,7 +67,7 @@ newQSem initial
sem <- newMVar (initial, [], [])
return (QSem sem)
--- |Wait for a unit to become available
+-- |Wait for a unit to become available.
waitQSem :: QSem -> IO ()
waitQSem (QSem m) =
mask_ $ do
@@ -91,7 +91,7 @@ waitQSem (QSem m) =
else do putMVar b (); return (i,b1,b2)
putMVar m r')
--- |Signal that a unit of the 'QSem' is available
+-- |Signal that a unit of the 'QSem' is available.
signalQSem :: QSem -> IO ()
signalQSem (QSem m) =
uninterruptibleMask_ $ do -- Note [signal uninterruptible]
=====================================
libraries/base/Control/Concurrent/QSemN.hs
=====================================
@@ -32,12 +32,12 @@ import Data.IORef (IORef, newIORef, atomicModifyIORef)
import System.IO.Unsafe (unsafePerformIO)
-- | 'QSemN' is a quantity semaphore in which the resource is acquired
--- and released in units of one. It provides guaranteed FIFO ordering
+-- and released in arbitrary amounts. It provides guaranteed FIFO ordering
-- for satisfying blocked `waitQSemN` calls.
--
-- The pattern
--
--- > bracket_ (waitQSemN n) (signalQSemN n) (...)
+-- > bracket_ (waitQSemN n) (signalQSemN n) (...)
--
-- is safe; it never loses any of the resource.
--
@@ -71,7 +71,7 @@ newQSemN initial
-- An unboxed version of Maybe (MVar a)
data MaybeMV a = JustMV !(MVar a) | NothingMV
--- |Wait for the specified quantity to become available
+-- |Wait for the specified quantity to become available.
waitQSemN :: QSemN -> Int -> IO ()
-- We need to mask here. Once we've enqueued our MVar, we need
-- to be sure to wait for it. Otherwise, we could lose our
=====================================
libraries/base/GHC/MVar.hs
=====================================
@@ -42,9 +42,11 @@ as a box, which may be empty or full.
-}
-- pull in Eq (Mvar a) too, to avoid GHC.Conc being an orphan-instance module
--- | @since 4.1.0.0
+-- | Compares the underlying pointers.
+--
+-- @since 4.1.0.0
instance Eq (MVar a) where
- (MVar mvar1#) == (MVar mvar2#) = isTrue# (sameMVar# mvar1# mvar2#)
+ (MVar mvar1#) == (MVar mvar2#) = isTrue# (sameMVar# mvar1# mvar2#)
{-
M-Vars are rendezvous points for concurrent threads. They begin
@@ -66,9 +68,9 @@ newEmptyMVar = IO $ \ s# ->
-- |Create an 'MVar' which contains the supplied value.
newMVar :: a -> IO (MVar a)
-newMVar value =
- newEmptyMVar >>= \ mvar ->
- putMVar mvar value >>
+newMVar value = do
+ mvar <- newEmptyMVar
+ putMVar mvar value
return mvar
-- |Return the contents of the 'MVar'. If the 'MVar' is currently
@@ -95,6 +97,7 @@ takeMVar (MVar mvar#) = IO $ \ s# -> takeMVar# mvar# s#
--
-- 'readMVar' is multiple-wakeup, so when multiple readers are
-- blocked on an 'MVar', all of them are woken up at the same time.
+-- The runtime guarantees that all woken threads complete their 'readMVar' operation.
--
-- /Compatibility note:/ Prior to base 4.7, 'readMVar' was a combination
-- of 'takeMVar' and 'putMVar'. This mean that in the presence of
@@ -104,12 +107,12 @@ takeMVar (MVar mvar#) = IO $ \ s# -> takeMVar# mvar# s#
-- can be recovered by implementing 'readMVar as follows:
--
-- @
--- readMVar :: MVar a -> IO a
--- readMVar m =
--- mask_ $ do
--- a <- takeMVar m
--- putMVar m a
--- return a
+-- readMVar :: MVar a -> IO a
+-- readMVar m =
+-- mask_ $ do
+-- a <- takeMVar m
+-- putMVar m a
+-- return a
-- @
readMVar :: MVar a -> IO a
readMVar (MVar mvar#) = IO $ \ s# -> readMVar# mvar# s#
=====================================
testsuite/tests/linear/should_compile/T23814.hs
=====================================
@@ -0,0 +1,17 @@
+{-# LANGUAGE LinearTypes #-}
+{-# LANGUAGE MultiWayIf #-}
+
+module T23814 where
+
+f :: Bool -> Int %1 -> Int
+f b x =
+ if
+ | b -> x
+ | otherwise -> x
+
+g :: Bool -> Bool -> Int %1 -> Int %1 -> (Int, Int)
+g b c x y =
+ if
+ | b -> (x,y)
+ | c -> (y,x)
+ | otherwise -> (x,y)
=====================================
testsuite/tests/linear/should_compile/all.T
=====================================
@@ -42,3 +42,4 @@ test('T20023', normal, compile, [''])
test('T22546', normal, compile, [''])
test('T23025', normal, compile, ['-dlinear-core-lint'])
test('LinearRecUpd', normal, compile, [''])
+test('T23814', normal, compile, [''])
=====================================
testsuite/tests/linear/should_fail/T23814fail.hs
=====================================
@@ -0,0 +1,17 @@
+{-# LANGUAGE LinearTypes #-}
+{-# LANGUAGE MultiWayIf #-}
+
+module T23814fail where
+
+f' :: Bool -> Int %1 -> Int
+f' b x =
+ if
+ | b -> x
+ | otherwise -> 0
+
+g' :: Bool -> Bool -> Int %1 -> Int
+g' b c x =
+ if
+ | b -> x
+ | c -> 0
+ | otherwise -> 0
=====================================
testsuite/tests/linear/should_fail/T23814fail.stderr
=====================================
@@ -0,0 +1,17 @@
+
+T23814fail.hs:7:6: error: [GHC-18872]
+ • Couldn't match type ‘Many’ with ‘One’
+ arising from multiplicity of ‘x’
+ • In an equation for ‘f'’:
+ f' b x
+ = if | b -> x
+ | otherwise -> 0
+
+T23814fail.hs:13:8: error: [GHC-18872]
+ • Couldn't match type ‘Many’ with ‘One’
+ arising from multiplicity of ‘x’
+ • In an equation for ‘g'’:
+ g' b c x
+ = if | b -> x
+ | c -> 0
+ | otherwise -> 0
=====================================
testsuite/tests/linear/should_fail/all.T
=====================================
@@ -41,3 +41,4 @@ test('T19120', normal, compile_fail, [''])
test('T20083', normal, compile_fail, ['-XLinearTypes'])
test('T19361', normal, compile_fail, [''])
test('T21278', normal, compile_fail, ['-XLinearTypes'])
+test('T23814fail', normal, compile_fail, [''])
=====================================
utils/dump-decls/Main.hs
=====================================
@@ -6,7 +6,8 @@ import GHC.Core.Class (classMinimalDef)
import GHC.Core.TyCo.FVs (tyConsOfType)
import GHC.Driver.Ppr (showSDocForUser)
import GHC.Unit.State (lookupUnitId, lookupPackageName)
-import GHC.Unit.Info (UnitInfo, unitExposedModules, PackageName(..))
+import GHC.Unit.Info (UnitInfo, unitExposedModules, unitId, PackageName(..))
+import GHC.Unit.Types (UnitId)
import GHC.Data.FastString (fsLit)
import GHC.Driver.Env (hsc_units, hscEPS)
import GHC.Utils.Outputable
@@ -163,14 +164,14 @@ reportUnitDecls :: UnitInfo -> Ghc SDoc
reportUnitDecls unit_info = do
let exposed :: [ModuleName]
exposed = map fst (unitExposedModules unit_info)
- vcat <$> mapM reportModuleDecls exposed
+ vcat <$> mapM (reportModuleDecls $ unitId unit_info) exposed
-reportModuleDecls :: ModuleName -> Ghc SDoc
-reportModuleDecls modl_nm
+reportModuleDecls :: UnitId -> ModuleName -> Ghc SDoc
+reportModuleDecls unit_id modl_nm
| modl_nm `elem` ignoredModules = do
return $ vcat [ mod_header, text "-- ignored", text "" ]
| otherwise = do
- modl <- GHC.lookupQualifiedModule NoPkgQual modl_nm
+ modl <- GHC.lookupQualifiedModule (OtherPkg unit_id) modl_nm
mb_mod_info <- GHC.getModuleInfo modl
mod_info <- case mb_mod_info of
Nothing -> fail "Failed to find module"
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/765d6dfd4e573fbba57fe3ea4ed8dbe61fff8964...d5c835cafadae5fadfd9d69581efceb16b637407
--
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/765d6dfd4e573fbba57fe3ea4ed8dbe61fff8964...d5c835cafadae5fadfd9d69581efceb16b637407
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/20230822/752c874b/attachment-0001.html>
More information about the ghc-commits
mailing list