scheduler 2.0.0.1 → 2.0.1.0
raw patch · 8 files changed
+121/−87 lines, 8 filesdep +quickcheck-classesdep −doctestdep −genvalidity-hspecdep −mwc-randomdep ~QuickCheckdep ~basedep ~primitive
Dependencies added: quickcheck-classes
Dependencies removed: doctest, genvalidity-hspec, mwc-random, vector
Dependency ranges changed: QuickCheck, base, primitive, unliftio-core
Files
- CHANGELOG.md +16/−9
- scheduler.cabal +20/−12
- src/Control/Scheduler.hs +27/−27
- src/Control/Scheduler/Computation.hs +1/−1
- src/Control/Scheduler/Internal.hs +7/−6
- src/Control/Scheduler/Types.hs +17/−0
- tests/Control/SchedulerSpec.hs +32/−18
- tests/doctests.hs +1/−14
CHANGELOG.md view
@@ -1,5 +1,12 @@-# 2.0.0+# `scheduler` +## 2.0.1++* Add `Eq1` and `Show1` instances for `Result`+* Add compatibility with older `primitive`++## 2.0.0+ * Switch type parameter of `Scheduler` from monad `m` to state token `s`. Which also means that constraints on many functions got tighter (i.e. `MonadPrim`, `MonadPrimBase`) * Remove `m` type parameter for `SchedulerWS`, since it can only work in `IO` like monad anyways.@@ -7,7 +14,7 @@ * Swap order of arguments for `replicateWork` for consistency * Add `replicateWork_` that is slightly more efficient than `replicateWork` -# 1.5.0+## 1.5.0 Despite that the major part of the version was bumped up, this release does not include any breaking changes, only improvements and additions.@@ -24,7 +31,7 @@ * Addition of `GlobalScheduler` that can be reused throughout the codebase, thus reducing initialization overhead. -# 1.4.2+## 1.4.2 * Add `withTrivialScheduler` * Add `Results` data type as well as corresponding functions:@@ -32,11 +39,11 @@ * `withSchedulerWSR` * `withTrivialSchedulerR` -# 1.4.1+## 1.4.1 * Add functions: `replicateWork` -# 1.4.0+## 1.4.0 * Worker id has been promoted from `Int` to a `newtype` wrapper `WorkerId`. * Addition of `SchedulerWS` and `WorkerStates` data types. As well as the@@ -51,18 +58,18 @@ * `unwrapSchedulerWS` * Made internal modules accessible, but invisible. -# 1.3.0+## 1.3.0 * Make sure internal `Scheduler` accessor functions are no longer exported, they only cause breakage. * Make sure number of capabilities does not change through out the program execution, as far as `scheduler` is concerned. -# 1.2.0+## 1.2.0 * Addition of `scheduleWorkId` and `scheduleWorkId_` -# 1.1.0+## 1.1.0 * Add functions: `replicateConcurrently` and `replicateConcurrently_` * Made `traverseConcurrently_` lazy, thus making it possible to apply to infinite lists and other such@@ -70,6 +77,6 @@ * Fix `Monoid` instance for `Comp` * Addition of `Par'` pattern -# 1.0.0+## 1.0.0 Initial release.
scheduler.cabal view
@@ -1,8 +1,8 @@ name: scheduler-version: 2.0.0.1+version: 2.0.1.0 synopsis: Work stealing scheduler. description: A work stealing scheduler that is designed for parallelization of heavy work loads. It was primarily developed for [massiv](https://github.com/lehins/massiv) array library, but it is general enough to be useful for any computation that fits the model of few workers and many jobs.-homepage: https://github.com/lehins/haskell-scheduler+homepage: https://github.com/lehins/scheduler license: BSD3 license-file: LICENSE author: Alexey Kuleshevich@@ -13,6 +13,19 @@ extra-source-files: README.md , CHANGELOG.md cabal-version: >=1.10+tested-with: GHC == 8.0.2+ , GHC == 8.2.2+ , GHC == 8.4.4+ , GHC == 8.6.5+ , GHC == 8.8.6+ , GHC == 8.10.7+ , GHC == 9.0.2+ , GHC == 9.2.8+ , GHC == 9.4.8+ , GHC == 9.6.6+ , GHC == 9.8.4+ , GHC == 9.10.1+ , GHC == 9.12.1 library hs-source-dirs: src@@ -26,8 +39,8 @@ , atomic-primops , deepseq , exceptions- , unliftio-core- , primitive >= 0.7.1+ , unliftio-core >= 0.1.1+ , primitive >= 0.6.4 , pvar < 2.0 default-language: Haskell2010 ghc-options: -Wall -Wno-simplifiable-class-constraints@@ -40,12 +53,12 @@ other-modules: Spec , Control.SchedulerSpec build-tool-depends: hspec-discover:hspec-discover- build-depends: base+ build-depends: base >= 4.9 && < 5 , deepseq- , genvalidity-hspec , scheduler , hspec , QuickCheck+ , quickcheck-classes >= 0.6 , unliftio default-language: Haskell2010@@ -55,12 +68,7 @@ type: exitcode-stdio-1.0 hs-source-dirs: tests main-is: doctests.hs- build-depends: base- if impl(ghc >= 8.2) && impl(ghc < 8.10)- build-depends: doctest >=0.15- , mwc-random- , scheduler- , vector+ build-depends: base >= 4.9 && < 5 default-language: Haskell2010 source-repository head
src/Control/Scheduler.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE MonoLocalBinds #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} -- | -- Module : Control.Scheduler -- Copyright : (c) Alexey Kuleshevich 2018-2021@@ -162,7 +162,7 @@ -- | Schedule a job that will get a worker state passed as an argument -- -- @since 1.4.0-scheduleWorkState :: MonadPrimBase RealWorld m => SchedulerWS ws a -> (ws -> m a) -> m ()+scheduleWorkState :: (PrimBase m, RealWorld ~ PrimState m) => SchedulerWS ws a -> (ws -> m a) -> m () scheduleWorkState schedulerS withState = scheduleWorkId (_getScheduler schedulerS) $ \(WorkerId i) -> withState (indexSmallArray (_workerStatesArray (_workerStates schedulerS)) i)@@ -170,7 +170,7 @@ -- | Same as `scheduleWorkState`, but dont' keep the result of computation. -- -- @since 1.4.0-scheduleWorkState_ :: MonadPrimBase RealWorld m => SchedulerWS ws () -> (ws -> m ()) -> m ()+scheduleWorkState_ :: (PrimBase m, RealWorld ~ PrimState m) => SchedulerWS ws () -> (ws -> m ()) -> m () scheduleWorkState_ schedulerS withState = scheduleWorkId_ (_getScheduler schedulerS) $ \(WorkerId i) -> withState (indexSmallArray (_workerStatesArray (_workerStates schedulerS)) i)@@ -191,7 +191,7 @@ -- scheduler. -- -- @since 1.2.0-scheduleWorkId :: MonadPrimBase s m => Scheduler s a -> (WorkerId -> m a) -> m ()+scheduleWorkId :: (PrimBase m, s ~ PrimState m) => Scheduler s a -> (WorkerId -> m a) -> m () scheduleWorkId s f = stToPrim (_scheduleWorkId s (primToPrim . f)) -- | As soon as possible try to terminate any computation that is being performed by all@@ -203,7 +203,7 @@ -- although it will make sure their results are discarded. -- -- @since 1.1.0-terminate :: MonadPrim s m => Scheduler s a -> a -> m a+terminate :: (PrimMonad m, s ~ PrimState m) => Scheduler s a -> a -> m a terminate scheduler a = stToPrim (_terminate scheduler (Early a)) -- | Same as `terminate`, but returning a single element list containing the supplied@@ -216,14 +216,14 @@ -- function. -- -- @since 1.1.0-terminateWith :: MonadPrim s m => Scheduler s a -> a -> m a+terminateWith :: (PrimMonad m, s ~ PrimState m) => Scheduler s a -> a -> m a terminateWith scheduler a = stToPrim $ _terminate scheduler (EarlyWith a) -- | Schedule an action to be picked up and computed by a worker from a pool of -- jobs. Similar to `scheduleWorkId`, except the job doesn't get the worker id. -- -- @since 1.0.0-scheduleWork :: MonadPrimBase s m => Scheduler s a -> m a -> m ()+scheduleWork :: (PrimBase m, s ~ PrimState m) => Scheduler s a -> m a -> m () scheduleWork scheduler f = stToPrim $ _scheduleWorkId scheduler (const (primToPrim f)) @@ -232,13 +232,13 @@ -- | Same as `scheduleWork`, but only for a `Scheduler` that doesn't keep the results. -- -- @since 1.1.0-scheduleWork_ :: MonadPrimBase s m => Scheduler s () -> m () -> m ()+scheduleWork_ :: (PrimBase m, s ~ PrimState m) => Scheduler s () -> m () -> m () scheduleWork_ s = stToPrim . scheduleWork s . primToPrim -- | Same as `scheduleWorkId`, but only for a `Scheduler` that doesn't keep the results. -- -- @since 1.2.0-scheduleWorkId_ :: MonadPrimBase s m => Scheduler s () -> (WorkerId -> m ()) -> m ()+scheduleWorkId_ :: (PrimBase m, s ~ PrimState m) => Scheduler s () -> (WorkerId -> m ()) -> m () scheduleWorkId_ scheduler f = stToPrim $ _scheduleWorkId scheduler (primToPrim . f) -- | Schedule the same action to run @n@ times concurrently. This differs from@@ -247,7 +247,7 @@ -- To be called within a `withScheduler` block. -- -- @since 2.0.0-replicateWork :: MonadPrimBase s m => Scheduler s a -> Int -> m a -> m ()+replicateWork :: (PrimBase m, s ~ PrimState m) => Scheduler s a -> Int -> m a -> m () replicateWork scheduler n f = go n where go !k@@ -258,7 +258,7 @@ -- | Same as `replicateWork`, but it does not retain the results of scheduled jobs -- -- @since 2.0.0-replicateWork_ :: MonadPrimBase s m => Scheduler s () -> Int -> m a -> m ()+replicateWork_ :: (PrimBase m, s ~ PrimState m) => Scheduler s () -> Int -> m a -> m () replicateWork_ scheduler n f = go n where go !k@@ -270,7 +270,7 @@ -- /Important/ - In case of `Seq` computation strategy this function has no affect. -- -- @since 1.1.0-terminate_ :: MonadPrim s m => Scheduler s () -> m ()+terminate_ :: (PrimMonad m, s ~ PrimState m) => Scheduler s () -> m () terminate_ s = stToPrim $ _terminate s (Early ()) @@ -278,7 +278,7 @@ -- computation strategy, except it is restricted to `PrimMonad`, instead of `MonadUnliftIO`. -- -- @since 1.4.2-withTrivialScheduler :: MonadPrim s m => (Scheduler s a -> m b) -> m [a]+withTrivialScheduler :: (PrimMonad m, s ~ PrimState m) => (Scheduler s a -> m b) -> m [a] withTrivialScheduler action = F.toList <$> withTrivialSchedulerR action @@ -305,7 +305,8 @@ traverseConcurrently_ :: (MonadUnliftIO m, Foldable t) => Comp -> (a -> m b) -> t a -> m () traverseConcurrently_ comp f xs = withRunInIO $ \run ->- withScheduler_ comp $ \s -> scheduleWork s $ F.traverse_ (scheduleWork s . void . run . f) xs+ withScheduler_ comp $ \s -> F.traverse_ (scheduleWork_ s . void . run . f) xs+{-# INLINE traverseConcurrently_ #-} -- | Replicate an action @n@ times and schedule them acccording to the supplied computation -- strategy.@@ -315,6 +316,7 @@ replicateConcurrently comp n f = withRunInIO $ \run -> withScheduler comp $ \s -> replicateM_ n $ scheduleWork s (run f)+{-# INLINE replicateConcurrently #-} -- | Just like `replicateConcurrently`, but discards the results of computation. --@@ -322,8 +324,8 @@ replicateConcurrently_ :: MonadUnliftIO m => Comp -> Int -> m a -> m () replicateConcurrently_ comp n f = withRunInIO $ \run -> do- withScheduler_ comp $ \s -> scheduleWork s $ replicateM_ n (scheduleWork s $ void $ run f)-+ withScheduler_ comp $ \s -> replicateM_ n (scheduleWork_ s $ void $ run f)+{-# INLINE replicateConcurrently_ #-} @@ -410,7 +412,7 @@ -- | Check if the supplied batch has already finished. -- -- @since 1.5.0-hasBatchFinished :: MonadPrim s m => Batch s a -> m Bool+hasBatchFinished :: (PrimMonad m, s ~ PrimState m) => Batch s a -> m Bool hasBatchFinished = stToPrim . batchHasFinished {-# INLINE hasBatchFinished #-} @@ -423,21 +425,21 @@ -- concurrent cancelation and it will return `True`. -- -- @since 1.5.0-cancelBatch :: MonadPrim s m => Batch s a -> a -> m Bool+cancelBatch :: (PrimMonad m, s ~ PrimState m) => Batch s a -> a -> m Bool cancelBatch b = stToPrim . batchCancel b {-# INLINE cancelBatch #-} -- | Same as `cancelBatch`, but only works with schedulers that don't care about results -- -- @since 1.5.0-cancelBatch_ :: MonadPrim s m => Batch s () -> m Bool+cancelBatch_ :: (PrimMonad m, s ~ PrimState m) => Batch s () -> m Bool cancelBatch_ b = stToPrim $ batchCancel b () {-# INLINE cancelBatch_ #-} -- | Same as `cancelBatch_`, but the result of computation will be set to `FinishedEarlyWith` -- -- @since 1.5.0-cancelBatchWith :: MonadPrim s m => Batch s a -> a -> m Bool+cancelBatchWith :: (PrimMonad m, s ~ PrimState m) => Batch s a -> a -> m Bool cancelBatchWith b = stToPrim . batchCancelWith b {-# INLINE cancelBatchWith #-} @@ -446,7 +448,7 @@ -- -- @since 1.5.0 getCurrentBatch ::- MonadPrim s m => Scheduler s a -> m (Batch s a)+ (PrimMonad m, s ~ PrimState m) => Scheduler s a -> m (Batch s a) getCurrentBatch scheduler = stToPrim $ do batchId <- _currentBatchId scheduler pure $ Batch@@ -471,7 +473,7 @@ -- scheduler prior to starting the batch it will end up on the batch result list. -- -- @since 1.5.0-runBatch :: MonadPrimBase s m => Scheduler s a -> (Batch s a -> m c) -> m [a]+runBatch :: (PrimBase m, s ~ PrimState m) => Scheduler s a -> (Batch s a -> m c) -> m [a] runBatch scheduler f = stToPrim $ do _ <- primToPrim . f =<< getCurrentBatch scheduler reverse . resultsToList <$> _waitForCurrentBatch scheduler@@ -481,7 +483,7 @@ -- -- @since 1.5.0 runBatch_ ::- MonadPrimBase s m => Scheduler s () -> (Batch s () -> m c) -> m ()+ (PrimBase m, s ~ PrimState m) => Scheduler s () -> (Batch s () -> m c) -> m () runBatch_ scheduler f = stToPrim $ do _ <- primToPrim . f =<< getCurrentBatch scheduler void (_waitForCurrentBatch scheduler)@@ -492,7 +494,7 @@ -- -- @since 1.5.0 runBatchR ::- MonadPrimBase s m => Scheduler s a -> (Batch s a -> m c) -> m (Results a)+ (PrimBase m, s ~ PrimState m) => Scheduler s a -> (Batch s a -> m c) -> m (Results a) runBatchR scheduler f = stToPrim $ do _ <- primToPrim . f =<< getCurrentBatch scheduler reverseResults <$> _waitForCurrentBatch scheduler@@ -520,7 +522,5 @@ False >>> didAWorkerDie $ withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False True->>> withScheduler Par $ \ s -> scheduleWork s $ myThreadId >>= killThread >> pure False-*** Exception: thread killed -}
src/Control/Scheduler/Computation.hs view
@@ -72,7 +72,7 @@ instance Monoid Comp where mempty = Seq {-# INLINE mempty #-}- mappend = joinComp+ mappend = (<>) {-# INLINE mappend #-} instance Semigroup Comp where
src/Control/Scheduler/Internal.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-} {-# OPTIONS_HADDOCK hide, not-home #-} -- | -- Module : Control.Scheduler.Internal@@ -115,23 +116,23 @@ -- rather computed immediately. -- -- @since 1.4.2-withTrivialSchedulerR :: forall a b m s. MonadPrim s m => (Scheduler s a -> m b) -> m (Results a)+withTrivialSchedulerR :: forall a b m s. (PrimMonad m, s ~ PrimState m) => (Scheduler s a -> m b) -> m (Results a) withTrivialSchedulerR action = do resVar <- newMutVar [] batchVar <- newMutVar $ BatchId 0 finResVar <- newMutVar Nothing batchEarlyVar <- newMutVar Nothing- let bumpCurrentBatchId :: MonadPrim s m' => m' ()+ let bumpCurrentBatchId :: (PrimMonad m', s ~ PrimState m') => m' () bumpCurrentBatchId = atomicModifyMutVar' batchVar (\(BatchId x) -> (BatchId (x + 1), ()))- bumpBatchId :: MonadPrim s m' => BatchId -> m' Bool+ bumpBatchId :: (PrimMonad m', s ~ PrimState m') => BatchId -> m' Bool bumpBatchId (BatchId c) = atomicModifyMutVar' batchVar $ \b@(BatchId x) -> if x == c then (BatchId (x + 1), True) else (b, False)- takeBatchEarly :: MonadPrim s m' => m' (Maybe (Early a))+ takeBatchEarly :: (PrimMonad m', s ~ PrimState m') => m' (Maybe (Early a)) takeBatchEarly = atomicModifyMutVar' batchEarlyVar $ \mEarly -> (Nothing, mEarly)- takeResults :: MonadPrim s m' => m' [a]+ takeResults :: (PrimMonad m', s ~ PrimState m') => m' [a] takeResults = atomicModifyMutVar' resVar $ \res -> ([], res) _ <- action $
src/Control/Scheduler/Types.hs view
@@ -34,6 +34,7 @@ import Control.Exception import Control.Scheduler.Computation import Control.Scheduler.Queue+import Data.Functor.Classes import Data.IORef import Data.Primitive.SmallArray import Data.Primitive.PVar@@ -77,6 +78,22 @@ Finished xs -> Finished <$> traverse f xs FinishedEarly xs x -> FinishedEarly <$> traverse f xs <*> f x FinishedEarlyWith x -> FinishedEarlyWith <$> f x++instance Eq1 Results where+ liftEq f (Finished xs1) (Finished xs2) = liftEq f xs1 xs2+ liftEq f (FinishedEarly xs1 x1) (FinishedEarly xs2 x2) = liftEq f xs1 xs2 && f x1 x2+ liftEq f (FinishedEarlyWith x1) (FinishedEarlyWith x2) = f x1 x2+ liftEq _ _ _ = False++instance Show1 Results where+ liftShowsPrec f g n = \case+ Finished xs -> wrap (("Finished " ++) . g xs)+ FinishedEarly xs x -> wrap (("FinishedEarly " ++) . g xs . (" " ++) . f 11 x)+ FinishedEarlyWith x -> wrap (("FinishedEarlyWith " ++) . f 11 x)+ where+ wrap s+ | n <= 1 = s+ | otherwise = ('(' :) . s . (++ ")") data Jobs m a = Jobs { jobsNumWorkers :: {-# UNPACK #-} !Int
tests/Control/SchedulerSpec.hs view
@@ -20,16 +20,13 @@ import qualified Data.Foldable as F (toList, traverse_) import Data.IORef import Data.List (groupBy, sort, sortOn)+import Data.Proxy import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck+import Test.QuickCheck.Classes import Test.QuickCheck.Function import Test.QuickCheck.Monadic-import Test.Validity.Eq-import Test.Validity.Functor-import Test.Validity.Monoid-import Test.Validity.Ord-import Test.Validity.Show import UnliftIO.Async import UnliftIO.Exception hiding (assert) #if !MIN_VERSION_base(4,11,0)@@ -514,7 +511,6 @@ -- res' <- withSchedulerR comp scheduleJobs -- pure (res === [n] .&&. res' === FinishedEarlyWith n) - spec :: Spec spec = do describe "Comp" $ do@@ -525,27 +521,40 @@ property $ \(x :: Comp) y z -> x <> (y <> z) === (x <> y) <> z it "mconcat = foldr '(<>)' mempty" $ property $ \(xs :: [Comp]) -> mconcat xs === foldr (<>) mempty xs- eqSpecOnArbitrary @Comp- monoidSpecOnArbitrary @Comp+ it "Laws" $+ lawsCheckOne (Proxy @Comp)+ [ eqLaws+ , showLaws+ , semigroupLaws+ , monoidLaws+ ] describe "Show" $ do it "show == showsPrec 0" $ property $ \(x :: Comp) -> x `deepseq` show x === showsPrec 0 x "" it "(show) == showsPrec 1" $ property $ \(x :: Comp) (Positive n) -> x /= Seq && x /= Par ==> ("(" <> show x <> ")" === showsPrec n x "") describe "Results" $ do- eqSpecOnArbitrary @(Results Int)- functorSpecOnArbitrary @Results- showReadSpecOnArbitrary @(Results Int)+ it "Laws" $ do+ lawsCheckOne (Proxy @(Results Int))+ [ eqLaws+ , showLaws+ , showReadLaws+ ]+ lawsCheck (functorLaws (Proxy @Results)) it "Traversable" $ property $ \(rs :: Results Int) (f :: Fun Int (Maybe Int)) -> traverse (apply f) (F.toList rs) === fmap F.toList (traverse (apply f) rs) describe "WorkerId" $ do- eqSpecOnArbitrary @WorkerId- ordSpecOnArbitrary @WorkerId it "MaxMin" $ property $ \x y -> conjoin [ max (WorkerId x) (WorkerId y) === WorkerId (max x y) , min (WorkerId x) (WorkerId y) === WorkerId (min x y) ]- showReadSpecOnArbitrary @WorkerId+ it "Laws" $ do+ lawsCheckOne (Proxy @WorkerId)+ [ eqLaws+ , ordLaws+ , showLaws+ , showReadLaws+ ] describe "Enum" $ do it "toEnumFromEnum" $ property $ \ wid@(WorkerId i) -> toEnum (getWorkerId wid) === wid .&&. fromEnum wid === i@@ -621,13 +630,17 @@ arbitrary = WorkerId <$> arbitrary instance Arbitrary a => Arbitrary (Results a) where- arbitrary =+ arbitrary = liftArbitrary arbitrary++instance Arbitrary1 Results where+ liftArbitrary gen = oneof- [ Finished <$> arbitrary- , FinishedEarly <$> arbitrary <*> arbitrary- , FinishedEarlyWith <$> arbitrary+ [ Finished <$> listOf gen+ , FinishedEarly <$> listOf gen <*> gen+ , FinishedEarlyWith <$> gen ] +#if !MIN_VERSION_QuickCheck(2,15,0) -- | Assert a synchronous exception assertExceptionIO :: (NFData a, Exception exc) => (exc -> Bool) -- ^ Return True if that is the exception that was expected@@ -641,6 +654,7 @@ (do res <- action res `deepseq` return False) $ \exc -> displayException exc `deepseq` return (isExc exc) assert hasFailed+#endif assertAsyncExceptionIO :: (Exception e, NFData a) => (e -> Bool) -> IO a -> Property assertAsyncExceptionIO isAsyncExc action =
tests/doctests.hs view
@@ -1,17 +1,4 @@-{-# LANGUAGE CPP #-} module Main where -#if __GLASGOW_HASKELL__ >= 802 && __GLASGOW_HASKELL__ < 810--import Test.DocTest (doctest)- main :: IO ()-main = doctest ["src"]--#else---- TODO: fix doctest support-main :: IO ()-main = putStrLn "\nDoctests are not supported for ghc version 8.2 and prior as well as 8.10\n"--#endif+main = pure ()