rock 0.2.0.0 → 0.3.0.0
raw patch · 9 files changed
+560/−299 lines, 9 filesdep +constraintsdep +constraints-extrasdep +dependent-hashmapdep −containersdep −dependent-mapdep −protoludedep ~basedep ~dependent-sumdep ~deriving-compatPVP ok
version bump matches the API change (PVP)
Dependencies added: constraints, constraints-extras, dependent-hashmap, hashable, hedgehog, lifted-base, monad-control, transformers-base, unordered-containers
Dependencies removed: containers, dependent-map, protolude
Dependency ranges changed: base, dependent-sum, deriving-compat, mtl, transformers
API changes (from Hackage documentation)
- Rock.Core: Blocked :: !BlockedTask f a -> Result f a
- Rock.Core: Sequential :: m a -> Sequential m a
- Rock.Core: [Ap] :: !BlockedTask f (a -> b) -> !BlockedTask f a -> Block f b
- Rock.Core: [BlockedTask] :: Block f a -> (a -> Task f b) -> BlockedTask f b
- Rock.Core: [Fetch] :: f a -> Block f a
- Rock.Core: [runSequential] :: Sequential m a -> m a
- Rock.Core: data Block f a
- Rock.Core: data BlockedTask f a
- Rock.Core: data Result f a
- Rock.Core: inParallel :: Strategy
- Rock.Core: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Rock.Core.Sequential m)
- Rock.Core: instance Data.GADT.Compare.GCompare f => Data.GADT.Compare.GCompare (Rock.Core.Writer w f)
- Rock.Core: instance Data.GADT.Compare.GEq f => Data.GADT.Compare.GEq (Rock.Core.Writer w f)
- Rock.Core: instance GHC.Base.Applicative (Rock.Core.Result f)
- Rock.Core: instance GHC.Base.Functor (Rock.Core.BlockedTask f)
- Rock.Core: instance GHC.Base.Functor (Rock.Core.Result f)
- Rock.Core: instance GHC.Base.Functor m => GHC.Base.Functor (Rock.Core.Sequential m)
- Rock.Core: instance GHC.Base.Monad (Rock.Core.Result f)
- Rock.Core: instance GHC.Base.Monad m => GHC.Base.Applicative (Rock.Core.Sequential m)
- Rock.Core: instance GHC.Base.Monad m => GHC.Base.Monad (Rock.Core.Sequential m)
- Rock.Core: instance Rock.Core.MonadFetch f m => Rock.Core.MonadFetch f (Rock.Core.Sequential m)
- Rock.Core: invalidateReverseDependencies :: GCompare f => f a -> ReverseDependencies f -> DMap f g -> (ReverseDependencies f, DMap f g)
- Rock.Core: newtype Sequential m a
- Rock.Core: runBlock :: Strategy -> Rules f -> Block f a -> IO a
- Rock.Core: runBlockedTask :: Strategy -> Rules f -> BlockedTask f a -> IO a
- Rock.Core: sequentially :: Strategy
- Rock.Core: transFetchBlock :: (forall b. f b -> Task f' b) -> Block f a -> Task f' a
- Rock.Core: transFetchBlockedTask :: (forall b. f b -> Task f' b) -> BlockedTask f a -> Task f' a
- Rock.Core: type Strategy = forall a b. IO (a -> b) -> IO a -> IO b
- Rock.Traces: instance (Data.Dependent.Sum.ShowTag f Data.Functor.Identity.Identity, GHC.Show.Show a) => GHC.Show.Show (Rock.Traces.ValueDeps f a)
- Rock.Traces: instance Data.Dependent.Sum.ShowTag f Data.Functor.Identity.Identity => Data.Functor.Classes.Show1 (Rock.Traces.ValueDeps f)
+ Rock: Cyclic :: Some f -> Cyclic f
+ Rock: Done :: !a -> MemoEntry a
+ Rock: Input :: TaskKind
+ Rock: NonInput :: TaskKind
+ Rock: Started :: !ThreadId -> !MVar (Maybe a) -> MemoEntry a
+ Rock: Task :: ReaderT (Fetch f) IO a -> Task f a
+ Rock: [Writer] :: f a -> Writer w f (a, w)
+ Rock: [unTask] :: Task f a -> ReaderT (Fetch f) IO a
+ Rock: class Monad m => MonadFetch f m | m -> f
+ Rock: data MemoEntry a
+ Rock: data TaskKind
+ Rock: data Writer w f a
+ Rock: fetch :: (MonadFetch f m, MonadTrans t, MonadFetch f m1, m ~ t m1) => f a -> m a
+ Rock: memoise :: forall f g. (GEq f, Hashable (Some f)) => IORef (DHashMap f MVar) -> GenRules f g -> GenRules f g
+ Rock: memoiseWithCycleDetection :: forall f g. (Typeable f, GShow f, GEq f, Hashable (Some f)) => IORef (DHashMap f MemoEntry) -> IORef (HashMap ThreadId ThreadId) -> GenRules f g -> GenRules f g
+ Rock: newtype Cyclic f
+ Rock: newtype Task f a
+ Rock: reachableReverseDependencies :: (GEq f, Hashable (Some f)) => f a -> ReverseDependencies f -> (DHashMap f (Const ()), ReverseDependencies f)
+ Rock: runTask :: Rules f -> Task f a -> IO a
+ Rock: traceFetch :: (forall a. f a -> Task g ()) -> (forall a. f a -> a -> Task g ()) -> GenRules f g -> GenRules f g
+ Rock: track :: forall f g a. (GEq f, Hashable (Some f)) => (forall a'. f a' -> a' -> g a') -> Task f a -> Task f (a, DHashMap f g)
+ Rock: trackM :: forall f g a. (GEq f, Hashable (Some f)) => (forall a'. f a' -> a' -> Task f (g a')) -> Task f a -> Task f (a, DHashMap f g)
+ Rock: trackReverseDependencies :: (GEq f, Hashable (Some f)) => IORef (ReverseDependencies f) -> Rules f -> Rules f
+ Rock: transFetch :: (forall b. f b -> Task f' b) -> Task f a -> Task f' a
+ Rock: type GenRules f g = forall a. f a -> Task g a
+ Rock: type ReverseDependencies f = HashMap (Some f) (HashSet (Some f))
+ Rock: type Rules f = GenRules f f
+ Rock: verifyTraces :: forall f dep. (Hashable (Some f), GEq f, Has' Eq f dep, Typeable f, GShow f) => IORef (Traces f dep) -> (forall a. f a -> a -> Task f (dep a)) -> GenRules (Writer TaskKind f) f -> Rules f
+ Rock: writer :: forall f w g. (forall a. f a -> w -> Task g ()) -> GenRules (Writer w f) g -> GenRules f g
+ Rock.Core: Cyclic :: Some f -> Cyclic f
+ Rock.Core: Fetch :: (forall a. f a -> IO a) -> Fetch f
+ Rock.Core: Started :: !ThreadId -> !MVar (Maybe a) -> MemoEntry a
+ Rock.Core: data MemoEntry a
+ Rock.Core: instance (Data.GADT.Internal.GShow f, Data.Typeable.Internal.Typeable f) => GHC.Exception.Type.Exception (Rock.Core.Cyclic f)
+ Rock.Core: instance Control.Monad.Base.MonadBase GHC.Types.IO (Rock.Core.Task f)
+ Rock.Core: instance Control.Monad.Trans.Control.MonadBaseControl GHC.Types.IO (Rock.Core.Task f)
+ Rock.Core: instance Data.GADT.Internal.GCompare f => Data.GADT.Internal.GCompare (Rock.Core.Writer w f)
+ Rock.Core: instance Data.GADT.Internal.GEq f => Data.GADT.Internal.GEq (Rock.Core.Writer w f)
+ Rock.Core: instance Data.GADT.Internal.GShow f => GHC.Show.Show (Rock.Core.Cyclic f)
+ Rock.Core: memoiseWithCycleDetection :: forall f g. (Typeable f, GShow f, GEq f, Hashable (Some f)) => IORef (DHashMap f MemoEntry) -> IORef (HashMap ThreadId ThreadId) -> GenRules f g -> GenRules f g
+ Rock.Core: newtype Cyclic f
+ Rock.Core: newtype Fetch f
+ Rock.Core: reachableReverseDependencies :: (GEq f, Hashable (Some f)) => f a -> ReverseDependencies f -> (DHashMap f (Const ()), ReverseDependencies f)
+ Rock.Core: trackM :: forall f g a. (GEq f, Hashable (Some f)) => (forall a'. f a' -> a' -> Task f (g a')) -> Task f a -> Task f (a, DHashMap f g)
+ Rock.Traces: instance (Data.GADT.Internal.GShow f, Data.Constraint.Extras.Has' GHC.Show.Show f dep) => Data.Functor.Classes.Show1 (Rock.Traces.ValueDeps f dep)
+ Rock.Traces: instance (GHC.Show.Show a, Data.GADT.Internal.GShow f, Data.Constraint.Extras.Has' GHC.Show.Show f dep) => GHC.Show.Show (Rock.Traces.ValueDeps f dep a)
- Rock: type Traces f = DMap f (ValueDeps f)
+ Rock: type Traces f dep = DHashMap f (ValueDeps f dep)
- Rock.Core: Done :: a -> Result f a
+ Rock.Core: Done :: !a -> MemoEntry a
- Rock.Core: Task :: IO (Result f a) -> Task f a
+ Rock.Core: Task :: ReaderT (Fetch f) IO a -> Task f a
- Rock.Core: [unTask] :: Task f a -> IO (Result f a)
+ Rock.Core: [unTask] :: Task f a -> ReaderT (Fetch f) IO a
- Rock.Core: memoise :: forall f g. GCompare f => MVar (DMap f MVar) -> GenRules f g -> GenRules f g
+ Rock.Core: memoise :: forall f g. (GEq f, Hashable (Some f)) => IORef (DHashMap f MVar) -> GenRules f g -> GenRules f g
- Rock.Core: runTask :: Strategy -> Rules f -> Task f a -> IO a
+ Rock.Core: runTask :: Rules f -> Task f a -> IO a
- Rock.Core: track :: forall f a. GCompare f => Task f a -> Task f (a, DMap f Identity)
+ Rock.Core: track :: forall f g a. (GEq f, Hashable (Some f)) => (forall a'. f a' -> a' -> g a') -> Task f a -> Task f (a, DHashMap f g)
- Rock.Core: trackReverseDependencies :: GCompare f => MVar (ReverseDependencies f) -> Rules f -> Rules f
+ Rock.Core: trackReverseDependencies :: (GEq f, Hashable (Some f)) => IORef (ReverseDependencies f) -> Rules f -> Rules f
- Rock.Core: type ReverseDependencies f = Map (Some f) (Set (Some f))
+ Rock.Core: type ReverseDependencies f = HashMap (Some f) (HashSet (Some f))
- Rock.Core: verifyTraces :: (EqTag f Identity, GCompare f) => MVar (Traces f) -> GenRules (Writer TaskKind f) f -> Rules f
+ Rock.Core: verifyTraces :: forall f dep. (Hashable (Some f), GEq f, Has' Eq f dep, Typeable f, GShow f) => IORef (Traces f dep) -> (forall a. f a -> a -> Task f (dep a)) -> GenRules (Writer TaskKind f) f -> Rules f
- Rock.Traces: ValueDeps :: !a -> !DMap f Identity -> ValueDeps f a
+ Rock.Traces: ValueDeps :: !a -> !DHashMap f dep -> ValueDeps f dep a
- Rock.Traces: [dependencies] :: ValueDeps f a -> !DMap f Identity
+ Rock.Traces: [dependencies] :: ValueDeps f dep a -> !DHashMap f dep
- Rock.Traces: [value] :: ValueDeps f a -> !a
+ Rock.Traces: [value] :: ValueDeps f dep a -> !a
- Rock.Traces: data ValueDeps f a
+ Rock.Traces: data ValueDeps f dep a
- Rock.Traces: record :: GCompare f => f a -> a -> DMap f Identity -> Traces f -> Traces f
+ Rock.Traces: record :: (GEq f, Hashable (Some f)) => f a -> a -> DHashMap f g -> Traces f g -> Traces f g
- Rock.Traces: type Traces f = DMap f (ValueDeps f)
+ Rock.Traces: type Traces f dep = DHashMap f (ValueDeps f dep)
- Rock.Traces: verifyDependencies :: (Monad m, EqTag f Identity) => (forall a'. f a' -> m a') -> ValueDeps f a -> m (Maybe a)
+ Rock.Traces: verifyDependencies :: (MonadIO m, GEq f, Has' Eq f dep) => (forall a'. f a' -> m a') -> (forall a'. f a' -> a' -> m (dep a')) -> ValueDeps f dep a -> m (Maybe a)
Files
- CHANGELOG.md +13/−0
- LICENSE +1/−1
- README.md +27/−37
- examples/Spreadsheet.hs +24/−27
- rock.cabal +49/−11
- src/Rock.hs +1/−1
- src/Rock/Core.hs +213/−202
- src/Rock/Traces.hs +26/−20
- tests/Main.hs +206/−0
CHANGELOG.md view
@@ -1,5 +1,18 @@ # Unreleased +- Add `memoiseWithCycleDetection` and `Cycle`, enabling cycle detection+- Implement `Task` using `ReaderT`, improving performance+- Make buildable with GHC 8.2.2 through 8.8.3+- Switch from the `dependent-map` package to the `dependent-hashmap` for caches+- Remove support for Haxl-style automatic parallelisation+ * Remove strategy parameter from `runTask`+ * Add `MonadBaseControl`, which allows manual parallelisation using e.g. lifted-async+ * Remove `Sequential` type+- Use `IORef`s instead of `MVar`s+- Add `trackM` function+- Remove `invalidateReverseDependencies` in favour of `reachableReverseDependencies`+- Generalise `verifyTraces` to verify using user-supplied data+ # 0.2.0.0 - Stop using hashes when verifying traces (gets rid of the `Rock.HashTag` and `Rock.Hashed` modules)
LICENSE view
@@ -1,4 +1,4 @@-Copyright Olle Fredriksson (c) 2018-2019+Copyright Olle Fredriksson (c) 2018-2020 All rights reserved.
README.md view
@@ -1,6 +1,6 @@-# rock [](https://hackage.haskell.org/package/rock)+# rock [](https://travis-ci.com/ollef/rock) [](https://hackage.haskell.org/package/rock) -A build system inspired by [Build systems à la carte](https://www.microsoft.com/en-us/research/publication/build-systems-la-carte/) and [Haxl](http://hackage.haskell.org/package/haxl).+A build system inspired by [Build systems à la carte](https://www.microsoft.com/en-us/research/publication/build-systems-la-carte/). Used in [Sixten](https://github.com/ollef/sixten) and [Sixty](https://github.com/ollef/sixty) to achieve incremental and query driven@@ -9,15 +9,16 @@ # Example ```haskell+{-# language FlexibleInstances #-} {-# language GADTs #-}-{-# language NoImplicitPrelude #-}-{-# language OverloadedStrings #-} {-# language StandaloneDeriving #-} {-# language TemplateHaskell #-} -import Protolude--import Data.GADT.Compare.TH (deriveGEq, deriveGCompare)+import Control.Monad.IO.Class+import Data.GADT.Compare.TH (deriveGEq)+import Data.Hashable+import Data.Some+import Data.IORef import qualified Rock data Query a where@@ -27,13 +28,22 @@ D :: Query Integer deriving instance Show (Query a)- deriveGEq ''Query-deriveGCompare ''Query +instance Hashable (Query a) where+ hashWithSalt salt query =+ case query of+ A -> hashWithSalt salt (0 :: Int)+ B -> hashWithSalt salt (1 :: Int)+ C -> hashWithSalt salt (2 :: Int)+ D -> hashWithSalt salt (3 :: Int)++instance Hashable (Some Query) where+ hashWithSalt salt (Some query) = hashWithSalt salt query+ rules :: Rock.Rules Query rules key = do- putText $ "Fetching " <> show key+ liftIO $ putStrLn $ "Fetching " <> show key case key of A -> pure 10 B -> do@@ -48,27 +58,14 @@ main :: IO () main = do do- putText "Running"- result <- Rock.runTask Rock.sequentially rules (Rock.fetch D)- print result- do- putText "Running with memoisation"- memoVar <- newMVar mempty- result <-- Rock.runTask- Rock.sequentially- (Rock.memoise memoVar rules)- (Rock.fetch D)+ liftIO $ putStrLn "Running"+ result <- Rock.runTask rules (Rock.fetch D) print result do- putText "Running with memoisation using the parallel strategy"- memoVar <- newMVar mempty- result <-- Rock.runTask- Rock.inParallel- (Rock.memoise memoVar rules)- (Rock.fetch D)- print result+ liftIO $ putStrLn "Running with memoisation"+ memoVar <- newIORef mempty+ result <- Rock.runTask (Rock.memoise memoVar rules) (Rock.fetch D)+ liftIO $ print result ``` Prints@@ -87,12 +84,6 @@ Fetching A Fetching C 70-Running with memoisation using the parallel strategy-Fetching D-Fetching C-Fetching B-Fetching A-70 ``` # Related projects@@ -102,5 +93,4 @@ # Contributions -... are very welcome, especially in the areas of documentation, examples,-testing, and benchmarking.+... are very welcome, especially in the areas of documentation and examples.
examples/Spreadsheet.hs view
@@ -1,12 +1,13 @@+{-# language FlexibleInstances #-} {-# language GADTs #-}-{-# language NoImplicitPrelude #-}-{-# language OverloadedStrings #-} {-# language StandaloneDeriving #-} {-# language TemplateHaskell #-} -import Protolude--import Data.GADT.Compare.TH (deriveGEq, deriveGCompare)+import Control.Monad.IO.Class+import Data.GADT.Compare.TH (deriveGEq)+import Data.Hashable+import Data.Some+import Data.IORef import qualified Rock data Query a where@@ -16,13 +17,22 @@ D :: Query Integer deriving instance Show (Query a)- deriveGEq ''Query-deriveGCompare ''Query +instance Hashable (Query a) where+ hashWithSalt salt query =+ case query of+ A -> hashWithSalt salt (0 :: Int)+ B -> hashWithSalt salt (1 :: Int)+ C -> hashWithSalt salt (2 :: Int)+ D -> hashWithSalt salt (3 :: Int)++instance Hashable (Some Query) where+ hashWithSalt salt (Some query) = hashWithSalt salt query+ rules :: Rock.Rules Query rules key = do- putText $ "Fetching " <> show key+ liftIO $ putStrLn $ "Fetching " <> show key case key of A -> pure 10 B -> do@@ -37,24 +47,11 @@ main :: IO () main = do do- putText "Running"- result <- Rock.runTask Rock.sequentially rules (Rock.fetch D)- print result- do- putText "Running with memoisation"- memoVar <- newMVar mempty- result <-- Rock.runTask- Rock.sequentially- (Rock.memoise memoVar rules)- (Rock.fetch D)+ liftIO $ putStrLn "Running"+ result <- Rock.runTask rules (Rock.fetch D) print result do- putText "Running with memoisation using the parallel strategy"- memoVar <- newMVar mempty- result <-- Rock.runTask- Rock.inParallel- (Rock.memoise memoVar rules)- (Rock.fetch D)- print result+ liftIO $ putStrLn "Running with memoisation"+ memoVar <- newIORef mempty+ result <- Rock.runTask (Rock.memoise memoVar rules) (Rock.fetch D)+ liftIO $ print result
rock.cabal view
@@ -1,5 +1,5 @@ name: rock-version: 0.2.0.0+version: 0.3.0.0 synopsis: A build system for incremental, parallel, and demand-driven computations description: See <https://www.github.com/ollef/rock> for more information and@@ -10,13 +10,17 @@ license-file: LICENSE author: Olle Fredriksson maintainer: fredriksson.olle@gmail.com-copyright: 2018-2019 Olle Fredriksson+copyright: 2018-2020 Olle Fredriksson category: Development build-type: Simple extra-source-files: README.md CHANGELOG.md cabal-version: >=1.10+tested-with: GHC == 8.2.2+ , GHC == 8.4.3+ , GHC == 8.6.5+ , GHC == 8.8.3 library ghc-options: -Wall@@ -35,15 +39,19 @@ Rock.Core Rock.Traces build-depends: base >= 4.7 && < 5- , containers- , dependent-map- , dependent-sum- , deriving-compat- , mtl- , transformers- , protolude+ , constraints-extras >= 0.3+ , dependent-hashmap >= 0.1+ , dependent-sum >= 0.6+ , deriving-compat >= 0.5+ , hashable >= 1.3+ , lifted-base >= 0.2+ , monad-control >= 1.0+ , mtl >= 2.2+ , transformers >= 0.5+ , transformers-base >= 0.4+ , unordered-containers >= 0.2 default-language: Haskell2010- default-extensions: OverloadedStrings, NoImplicitPrelude+ default-extensions: OverloadedStrings source-repository head type: git@@ -62,4 +70,34 @@ -threaded hs-source-dirs: examples default-language: Haskell2010- build-depends: base, rock, protolude, dependent-sum-template+ build-depends: base+ , dependent-sum+ , dependent-sum-template+ , hashable+ , rock++test-suite test-rock+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: tests+ ghc-options: -Wall+ -Wcompat+ -Widentities+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wmissing-home-modules+ -Wpartial-fields+ -Wredundant-constraints+ -Wtabs+ -funbox-strict-fields+ build-depends: base >=4.7 && <5+ , constraints+ , constraints-extras+ , dependent-hashmap+ , dependent-sum+ , hashable+ , hedgehog+ , mtl+ , rock+ , unordered-containers+ default-language: Haskell2010
src/Rock.hs view
@@ -3,5 +3,5 @@ , Traces ) where -import Rock.Core+import Rock.Core hiding (Fetch) import Rock.Traces
src/Rock/Core.hs view
@@ -1,6 +1,5 @@ {-# language CPP #-} {-# language DefaultSignatures #-}-{-# language DeriveFunctor #-} {-# language FlexibleContexts #-} {-# language FlexibleInstances #-} {-# language FunctionalDependencies #-}@@ -8,30 +7,46 @@ {-# language GeneralizedNewtypeDeriving #-} {-# language RankNTypes #-} {-# language ScopedTypeVariables #-}+{-# language TupleSections #-}+{-# language TypeFamilies #-} {-# language UndecidableInstances #-} module Rock.Core where -#if MIN_VERSION_base(4,12,0)-import Protolude hiding (Ap)-#else-import Protolude-#endif-+import Control.Concurrent+import Control.Exception.Lifted+import Control.Monad.Base import Control.Monad.Cont+import Control.Monad.Except import Control.Monad.Identity+import Control.Monad.Reader import qualified Control.Monad.RWS.Lazy as Lazy import qualified Control.Monad.RWS.Strict as Strict import qualified Control.Monad.State.Lazy as Lazy import qualified Control.Monad.State.Strict as Strict+import Control.Monad.Trans.Control import Control.Monad.Trans.Maybe import qualified Control.Monad.Writer.Lazy as Lazy import qualified Control.Monad.Writer.Strict as Strict-import Data.Dependent.Map(DMap, GCompare)-import qualified Data.Dependent.Map as DMap+import Data.Bifunctor+import Data.Constraint.Extras+import Data.Dependent.HashMap (DHashMap)+import qualified Data.Dependent.HashMap as DHashMap import Data.Dependent.Sum-import Data.GADT.Compare-import qualified Data.Map as Map-import qualified Data.Set as Set+import Data.Foldable+import Data.Functor.Const+import Data.GADT.Compare (GEq, GCompare, geq, gcompare, GOrdering(..))+import Data.GADT.Show (GShow)+import Data.Hashable+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import Data.HashSet (HashSet)+import qualified Data.HashSet as HashSet+import Data.IORef+import Data.Maybe+import Data.Typeable+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup+#endif import Data.Some import Rock.Traces(Traces)@@ -50,20 +65,11 @@ -- | An @IO@ action that is allowed to make @f@ queries using the 'fetch' -- method from its 'MonadFetch' instance.-newtype Task f a = Task { unTask :: IO (Result f a) }---- | The result of a @Task@, which is either done or wanting to make one or--- more @f@ queries.-data Result f a- = Done a- | Blocked !(BlockedTask f a)--data BlockedTask f a where- BlockedTask :: Block f a -> (a -> Task f b) -> BlockedTask f b+newtype Task f a = Task { unTask :: ReaderT (Fetch f) IO a }+ deriving+ (Functor, Applicative, Monad, MonadIO, MonadBase IO) -data Block f a where- Fetch :: f a -> Block f a- Ap :: !(BlockedTask f (a -> b)) -> !(BlockedTask f a) -> Block f b+newtype Fetch f = Fetch (forall a. f a -> IO a) ------------------------------------------------------------------------------- -- * Fetch class@@ -92,60 +98,16 @@ ------------------------------------------------------------------------------- -- Instances -instance Functor (Task f) where- {-# INLINE fmap #-}- fmap f (Task t) = Task $ fmap f <$> t---- Note: This instance might not fully evaluate @t1@ before @t2@ in--- @t1 '<*>' t2@ in case @t1@ performs a query using 'fetch'. If this--- is not desirable, use 'Sequential'.-instance Applicative (Task f) where- {-# INLINE pure #-}- pure = Task . pure . Done- {-# INLINE (<*>) #-}- Task mrf <*> Task mrx = Task $ (<*>) <$> mrf <*> mrx--instance Monad (Task f) where- {-# INLINE (>>) #-}- (>>) = (*>)- {-# INLINE (>>=) #-}- Task ma >>= f = Task $ do- ra <- ma- case ra of- Done a -> unTask $ f a- Blocked (BlockedTask b k) -> return $ Blocked $ BlockedTask b $ k >=> f--instance MonadIO (Task f) where- {-# INLINE liftIO #-}- liftIO io = Task $ pure <$> io- instance MonadFetch f (Task f) where- fetch key = Task $ pure $ Blocked $ BlockedTask (Fetch key) pure--instance Functor (Result f) where- {-# INLINE fmap #-}- fmap f (Done x) = Done $ f x- fmap f (Blocked b) = Blocked $ f <$> b--instance Applicative (Result f) where- {-# INLINE pure #-}- pure = Done- {-# INLINE (<*>) #-}- Done f <*> Done x = Done $ f x- Done f <*> Blocked b = Blocked $ f <$> b- Blocked b <*> Done x = Blocked $ ($ x) <$> b- Blocked b1 <*> Blocked b2 = Blocked $ BlockedTask (Ap b1 b2) pure--instance Monad (Result f) where- {-# INLINE (>>) #-}- (>>) = (*>)- {-# INLINE (>>=) #-}- Done x >>= f = f x- Blocked (BlockedTask b t) >>= f = Blocked $ BlockedTask b $ t >=> Task . pure . f+ {-# INLINE fetch #-}+ fetch key = Task $ do+ io <- asks (\(Fetch fetch_) -> fetch_ key)+ liftIO io -instance Functor (BlockedTask f) where- {-# INLINE fmap #-}- fmap f (BlockedTask b t) = BlockedTask b $ fmap f <$> t+instance MonadBaseControl IO (Task f) where+ type StM (Task f) a = StM (ReaderT (Fetch f) IO) a+ liftBaseWith k = Task $ liftBaseWith $ \ma -> k $ ma . unTask+ restoreM = Task . restoreM ------------------------------------------------------------------------------- -- * Transformations@@ -155,151 +117,201 @@ :: (forall b. f b -> Task f' b) -> Task f a -> Task f' a-transFetch f task = Task $ do- result <- unTask task- case result of- Done a -> return $ Done a- Blocked b -> unTask $ transFetchBlockedTask f b--transFetchBlockedTask- :: (forall b. f b -> Task f' b)- -> BlockedTask f a- -> Task f' a-transFetchBlockedTask f (BlockedTask b t) = do- a <- transFetchBlock f b- transFetch f $ t a--transFetchBlock- :: (forall b. f b -> Task f' b)- -> Block f a- -> Task f' a-transFetchBlock f (Fetch k) = f k-transFetchBlock f (Ap b1 b2) = transFetchBlockedTask f b1 <*> transFetchBlockedTask f b2------------------------------------------------------------------------------------ * Strategies---- | A 'Strategy' specifies how two queries are performed in an 'Applicative'--- context.-type Strategy = forall a b. IO (a -> b) -> IO a -> IO b---- | Runs the two queries in sequence.-sequentially :: Strategy-sequentially = (<*>)---- | Runs the two queries in parallel.-inParallel :: Strategy-inParallel mf mx = withAsync mf $ \af -> do- x <- mx- f <- wait af- return $ f x---- | Uses the underlying instances, except for the Applicative instance which--- is defined in terms of 'return' and '(>>=)'.------ When used with 'Task', i.e. if you construct @m :: 'Sequential' ('Task' f)--- a@, this means that fetches within @m@ are done sequentially.-newtype Sequential m a = Sequential { runSequential :: m a }- deriving (Functor, Monad, MonadIO, MonadFetch f)---- | Defined in terms of 'return' and '(>>=)'.-instance Monad m => Applicative (Sequential m) where- {-# INLINE pure #-}- pure = Sequential . return- {-# INLINE (<*>) #-}- Sequential mf <*> Sequential mx = Sequential $ mf >>= \f -> fmap f mx+transFetch f (Task task) =+ Task $ ReaderT $ \fetch_ ->+ runReaderT task $ Fetch $ \key ->+ runReaderT (unTask $ f key) fetch_ ------------------------------------------------------------------------------- -- * Running tasks --- | Perform a 'Task', fetching dependency queries from the given 'Rules' function and using the given 'Strategy' for fetches in an 'Applicative' context.-runTask :: Strategy -> Rules f -> Task f a -> IO a-runTask strategy rules task = do- result <- unTask task- case result of- Done a -> return a- Blocked b -> runBlockedTask strategy rules b--runBlockedTask :: Strategy -> Rules f -> BlockedTask f a -> IO a-runBlockedTask strategy rules (BlockedTask b f) = do- a <- runBlock strategy rules b- runTask strategy rules $ f a--runBlock :: Strategy -> Rules f -> Block f a -> IO a-runBlock strategy rules (Fetch key) =- runTask strategy rules $ rules key-runBlock strategy rules (Ap bf bx) =- strategy (runBlockedTask strategy rules bf) (runBlockedTask strategy rules bx)+-- | Perform a 'Task', fetching dependency queries from the given 'Rules'+-- function.+runTask :: Rules f -> Task f a -> IO a+runTask rules (Task task) =+ runReaderT task $ Fetch $ runTask rules . rules ------------------------------------------------------------------------------- -- * Task combinators --- | Track the query dependencies of a 'Task' in a 'DMap'-track :: forall f a. GCompare f => Task f a -> Task f (a, DMap f Identity)-track task = do- depsVar <- liftIO $ newMVar mempty+-- | Track the query dependencies of a 'Task' in a 'DHashMap'.+track+ :: forall f g a. (GEq f, Hashable (Some f))+ => (forall a'. f a' -> a' -> g a')+ -> Task f a+ -> Task f (a, DHashMap f g)+track f =+ trackM $ \key -> pure . f key++-- | Track the query dependencies of a 'Task' in a 'DHashMap'. Monadic version.+trackM+ :: forall f g a. (GEq f, Hashable (Some f))+ => (forall a'. f a' -> a' -> Task f (g a'))+ -> Task f a+ -> Task f (a, DHashMap f g)+trackM f task = do+ depsVar <- liftIO $ newIORef mempty let record :: f b -> Task f b record key = do value <- fetch key- liftIO $ modifyMVar_ depsVar $ pure . DMap.insert key (Identity value)+ g <- f key value+ liftIO $ atomicModifyIORef depsVar $ (, ()) . DHashMap.insert key g return value result <- transFetch record task- deps <- liftIO $ readMVar depsVar+ deps <- liftIO $ readIORef depsVar return (result, deps) -- | Remember what @f@ queries have already been performed and their results in--- a 'DMap', and reuse them if a query is performed again a second time.+-- a 'DHashMap', and reuse them if a query is performed again a second time. ----- The 'DMap' should typically not be reused if there has been some change that+-- The 'DHashMap' should typically not be reused if there has been some change that -- might make a query return a different result. memoise :: forall f g- . GCompare f- => MVar (DMap f MVar)+ . (GEq f, Hashable (Some f))+ => IORef (DHashMap f MVar) -> GenRules f g -> GenRules f g-memoise startedVar rules (key :: f a) =- join $ liftIO $ modifyMVar startedVar $ \started ->- case DMap.lookup key started of- Nothing -> do- valueVar <- newEmptyMVar- return- ( DMap.insert key valueVar started- , do- value <- rules key- liftIO $ putMVar valueVar value- return value- )- Just valueVar ->- return (started, liftIO $ readMVar valueVar)+memoise startedVar rules (key :: f a) = do+ maybeValueVar <- DHashMap.lookup key <$> liftIO (readIORef startedVar)+ case maybeValueVar of+ Nothing -> do+ valueVar <- liftIO newEmptyMVar+ join $ liftIO $ atomicModifyIORef startedVar $ \started ->+ case DHashMap.alterLookup (Just . fromMaybe valueVar) key started of+ (Nothing, started') ->+ ( started'+ , do+ value <- rules key+ liftIO $ putMVar valueVar value+ return value+ ) + (Just valueVar', _started') ->+ (started, liftIO $ readMVar valueVar')++ Just valueVar ->+ liftIO $ readMVar valueVar++newtype Cyclic f = Cyclic (Some f)+ deriving Show++instance (GShow f, Typeable f) => Exception (Cyclic f)++data MemoEntry a+ = Started !ThreadId !(MVar (Maybe a))+ | Done !a++-- | Like 'memoise', but throw @'Cyclic' f@ if a query depends on itself, directly or+-- indirectly.+--+-- The 'HashMap' represents dependencies between threads and should not be+-- reused between invocations.+memoiseWithCycleDetection+ :: forall f g+ . (Typeable f, GShow f, GEq f, Hashable (Some f))+ => IORef (DHashMap f MemoEntry)+ -> IORef (HashMap ThreadId ThreadId)+ -> GenRules f g+ -> GenRules f g+memoiseWithCycleDetection startedVar depsVar rules =+ rules'+ where+ rules' (key :: f a) = do+ maybeEntry <- DHashMap.lookup key <$> liftIO (readIORef startedVar)+ case maybeEntry of+ Nothing -> do+ threadId <- liftIO myThreadId+ valueVar <- liftIO newEmptyMVar+ join $ liftIO $ atomicModifyIORef startedVar $ \started ->+ case DHashMap.alterLookup (Just . fromMaybe (Started threadId valueVar)) key started of+ (Nothing, started') ->+ ( started'+ , (do+ value <- rules key+ liftIO $ do+ atomicModifyIORef startedVar $ \started'' ->+ (DHashMap.insert key (Done value) started'', ())+ putMVar valueVar $ Just value+ return value+ ) `catch` \(e :: Cyclic f) ->+ (liftIO $ do+ atomicModifyIORef startedVar $ \started'' ->+ (DHashMap.delete key started'', ())+ putMVar valueVar Nothing+ throwIO e+ )+ )++ (Just entry, _started') ->+ (started, waitFor entry)++ Just entry ->+ waitFor entry+ where+ waitFor entry =+ case entry of+ Started onThread valueVar -> do+ threadId <- liftIO myThreadId+ join $ liftIO $ atomicModifyIORef depsVar $ \deps -> do+ let+ deps' =+ HashMap.insert threadId onThread deps++ if detectCycle threadId deps' then+ ( deps+ , throwIO $ Cyclic $ Some key+ )+ else+ ( deps'+ , do+ maybeValue <- liftIO $ readMVar valueVar+ liftIO $ atomicModifyIORef depsVar $ \deps'' -> (HashMap.delete threadId deps'', ())+ maybe (rules' key) return maybeValue+ )++ Done value ->+ return value++ detectCycle threadId deps =+ go threadId+ where+ go tid =+ case HashMap.lookup tid deps of+ Nothing -> False+ Just dep+ | dep == threadId -> True+ | otherwise -> go dep+ -- | Remember the results of previous @f@ queries and what their dependencies -- were then. -- -- If all dependencies of a 'NonInput' query are the same, reuse the old result. -- 'Input' queries are not reused. verifyTraces- :: (EqTag f Identity, GCompare f)- => MVar (Traces f)+ :: forall f dep+ . (Hashable (Some f), GEq f, Has' Eq f dep, Typeable f, GShow f)+ => IORef (Traces f dep)+ -> (forall a. f a -> a -> Task f (dep a)) -> GenRules (Writer TaskKind f) f -> Rules f-verifyTraces tracesVar rules key = do- traces <- liftIO $ readMVar tracesVar- maybeValue <- case DMap.lookup key traces of+verifyTraces tracesVar createDependencyRecord rules key = do+ traces <- liftIO $ readIORef tracesVar+ maybeValue <- case DHashMap.lookup key traces of Nothing -> return Nothing Just oldValueDeps ->- Traces.verifyDependencies fetch oldValueDeps+ Traces.verifyDependencies fetch createDependencyRecord oldValueDeps `catch` \(_ :: Cyclic f) ->+ pure Nothing case maybeValue of Nothing -> do- ((value, taskKind), deps) <- track $ rules $ Writer key+ ((value, taskKind), deps) <- trackM createDependencyRecord $ rules $ Writer key case taskKind of Input -> return () NonInput ->- liftIO $ modifyMVar_ tracesVar- $ pure- . Traces.record key value deps+ liftIO $ atomicModifyIORef tracesVar+ $ (, ()) . Traces.record key value deps return value Just value -> return value @@ -348,35 +360,34 @@ after key result return result -type ReverseDependencies f = Map (Some f) (Set (Some f))+type ReverseDependencies f = HashMap (Some f) (HashSet (Some f)) --- | Write reverse dependencies to the 'MVar'.+-- | Write reverse dependencies to the 'IORef. trackReverseDependencies- :: GCompare f- => MVar (ReverseDependencies f)+ :: (GEq f, Hashable (Some f))+ => IORef (ReverseDependencies f) -> Rules f -> Rules f trackReverseDependencies reverseDepsVar rules key = do- (res, deps) <- track $ rules key- unless (DMap.null deps) $ do- let newReverseDeps = Map.fromListWith (<>)- [ (This depKey, Set.singleton $ This key)- | depKey DMap.:=> _ <- DMap.toList deps+ (res, deps) <- track (\_ _ -> Const ()) $ rules key+ unless (DHashMap.null deps) $ do+ let newReverseDeps = HashMap.fromListWith (<>)+ [ (Some depKey, HashSet.singleton $ Some key)+ | depKey :=> Const () <- DHashMap.toList deps ]- liftIO $ modifyMVar_ reverseDepsVar $ pure . Map.unionWith (<>) newReverseDeps+ liftIO $ atomicModifyIORef reverseDepsVar $ (, ()) . HashMap.unionWith (<>) newReverseDeps pure res --- | @'invalidateReverseDependencies' key@ removes all keys reachable, by--- reverse dependency, from @key@ from the input 'DMap'. It also returns the+-- | @'reachableReverseDependencies' key@ returns all keys reachable, by+-- reverse dependency, from @key@ from the input 'DHashMap'. It also returns the -- reverse dependency map with those same keys removed.-invalidateReverseDependencies- :: GCompare f+reachableReverseDependencies+ :: (GEq f, Hashable (Some f)) => f a -> ReverseDependencies f- -> DMap f g- -> (ReverseDependencies f, DMap f g)-invalidateReverseDependencies key reverseDeps m =+ -> (DHashMap f (Const ()), ReverseDependencies f)+reachableReverseDependencies key reverseDeps = foldl'- (\(reverseDeps', m') (This key') -> invalidateReverseDependencies key' reverseDeps' m')- (Map.delete (This key) reverseDeps, DMap.delete key m)- (Set.toList $ Map.findWithDefault mempty (This key) reverseDeps)+ (\(m', reverseDeps') (Some key') -> first (<> m') $ reachableReverseDependencies key' reverseDeps')+ (DHashMap.singleton key $ Const (), HashMap.delete (Some key) reverseDeps)+ (HashSet.toList $ HashMap.lookupDefault mempty (Some key) reverseDeps)
src/Rock/Traces.hs view
@@ -5,37 +5,43 @@ {-# language UndecidableInstances #-} module Rock.Traces where -import Protolude--import Data.Dependent.Map(DMap, GCompare, DSum((:=>)))-import qualified Data.Dependent.Map as DMap+import Control.Monad.IO.Class+import Data.Constraint.Extras+import Data.Dependent.HashMap(DHashMap)+import qualified Data.Dependent.HashMap as DHashMap import Data.Dependent.Sum import Data.Functor.Classes+import Data.GADT.Compare+import Data.GADT.Show+import Data.Hashable+import Data.Some import Text.Show.Deriving -data ValueDeps f a = ValueDeps+data ValueDeps f dep a = ValueDeps { value :: !a- , dependencies :: !(DMap f Identity)+ , dependencies :: !(DHashMap f dep) } return [] -deriving instance (ShowTag f Identity, Show a) => Show (ValueDeps f a)+deriving instance (Show a, GShow f, Has' Show f dep) => Show (ValueDeps f dep a) -instance ShowTag f Identity => Show1 (ValueDeps f) where+instance (GShow f, Has' Show f dep) => Show1 (ValueDeps f dep) where liftShowsPrec = $(makeLiftShowsPrec ''ValueDeps) -type Traces f = DMap f (ValueDeps f)+type Traces f dep = DHashMap f (ValueDeps f dep) verifyDependencies- :: (Monad m, EqTag f Identity)+ :: (MonadIO m, GEq f, Has' Eq f dep) => (forall a'. f a' -> m a')- -> ValueDeps f a+ -> (forall a'. f a' -> a' -> m (dep a'))+ -> ValueDeps f dep a -> m (Maybe a)-verifyDependencies fetch (ValueDeps value_ deps) = do- upToDate <- allM (DMap.toList deps) $ \(depKey :=> depValue) -> do- depValue' <- fetch depKey- return $ eqTagged depKey depKey depValue $ Identity depValue'+verifyDependencies fetch createDependencyRecord (ValueDeps value_ deps) = do+ upToDate <- allM (DHashMap.toList deps) $ \(depKey :=> dep) -> do+ depValue <- fetch depKey+ newDep <- createDependencyRecord depKey depValue+ return $ eqTagged depKey depKey dep newDep return $ if upToDate then Just value_ else Nothing@@ -50,12 +56,12 @@ return False record- :: GCompare f+ :: (GEq f, Hashable (Some f)) => f a -> a- -> DMap f Identity- -> Traces f- -> Traces f+ -> DHashMap f g+ -> Traces f g+ -> Traces f g record k v deps- = DMap.insert k+ = DHashMap.insert k $ ValueDeps v deps
+ tests/Main.hs view
@@ -0,0 +1,206 @@+{-# language CPP #-}+{-# language FlexibleInstances #-}+{-# language GADTs #-}+{-# language MultiParamTypeClasses #-}+{-# language RankNTypes #-}+{-# language StandaloneDeriving #-}+{-# language TypeApplications #-}+{-# language TemplateHaskell #-}+{-# language TypeFamilies #-}+module Main where++import Control.Monad+import Control.Monad.Identity+import Control.Monad.IO.Class+import Data.Constraint.Extras+import Data.Constraint.Extras.TH+import qualified Data.Dependent.HashMap as DHashMap+import Data.Functor.Const+import Data.GADT.Compare+import Data.GADT.Show+import Data.Hashable+import Data.IORef+import Data.List+#if !MIN_VERSION_base(4,11,0)+import Data.Semigroup+#endif+import Data.Some+import Data.Type.Equality ((:~:)(Refl))+import Hedgehog+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Rock++data Key v where+ IntKey :: Int -> Key Int+ StringKey :: String -> Key String++deriving instance Show (Key v)++instance GShow Key where+ gshowsPrec = showsPrec++instance GShow (Writer w Key) where+ gshowsPrec d (Writer key) = showsPrec d key++instance Hashable (Some Key) where+ hashWithSalt salt (Some key) =+ case key of+ IntKey i -> hashWithSalt salt (0 :: Int, i)+ StringKey s -> hashWithSalt salt (1 :: Int, s)++deriveArgDict ''Key++instance GEq Key where+ geq (IntKey i1) (IntKey i2)+ | i1 == i2 =+ Just Refl+ geq (StringKey s1) (StringKey s2)+ | s1 == s2 =+ Just Refl+ geq _ _ =+ Nothing++instance GCompare Key where+ gcompare (IntKey i1) (IntKey i2)+ | i1 == i2 =+ GEQ+ | i1 < i2 =+ GLT+ | otherwise =+ GGT+ gcompare (IntKey _) _ =+ GLT+ gcompare _ (IntKey _) =+ GGT+ gcompare (StringKey s1) (StringKey s2)+ | s1 == s2 =+ GEQ+ | s1 < s2 =+ GLT+ | otherwise =+ GGT++int :: Gen Int+int = Gen.int (Range.linear 0 100)++string :: Gen String+string = Gen.string (Range.linear 0 100) Gen.ascii++key :: Gen (Some Key)+key =+ Gen.choice+ [ Some . IntKey <$> int+ , Some . StringKey <$> string+ ]++addRules :: Rules Key+addRules key_ =+ case key_ of+ IntKey i ->+ pure $ i + 1++ StringKey s ->+ pure $ s <> "a"++withKeyFetchedCallback :: (Some f -> IO ()) -> GenRules f g -> GenRules f g+withKeyFetchedCallback keyFetched rules key_ = do+ liftIO $ keyFetched $ Some key_+ rules key_++prop_track_tracks :: Property+prop_track_tracks =+ property $ do+ Some key_ <- forAll key+ startedVar <- liftIO $ newIORef mempty+ let+ rules :: Rules Key+ rules =+ memoise startedVar addRules++ ((), deps) <- liftIO $ runTask rules $ do+ void $ fetch key_+ track (\_ _ -> Const ()) $ void $ fetch key_++ DHashMap.keys deps === [Some key_]++prop_memoise_memoises :: Property+prop_memoise_memoises =+ property $ do+ Some key_ <- forAll key+ fetchedKeysVar <- liftIO $ newIORef []+ startedVar <- liftIO $ newIORef mempty+ let+ keyFetched k =+ atomicModifyIORef fetchedKeysVar $ \ks -> (k : ks, ())++ rules :: Rules Key+ rules =+ memoise startedVar (withKeyFetchedCallback keyFetched addRules)++ liftIO $ runTask rules $ do+ void $ fetch key_+ void $ fetch key_++ fetchedKeys <- liftIO $ readIORef fetchedKeysVar+ fetchedKeys === [Some key_]++inputRules :: Int -> GenRules (Writer TaskKind Key) Key+inputRules input (Writer key_) =+ case key_ of+ IntKey 0 -> do+ pure (input, Input)++ IntKey i -> do+ pure (i + 1, NonInput)++ StringKey "dependent" -> do+ i <- fetch $ IntKey 0+ j <- fetch $ IntKey 1+ pure (show i <> show j, NonInput)++ StringKey s -> do+ i <- fetch $ IntKey 1+ j <- fetch $ IntKey 2+ pure (s <> show i <> show j, NonInput)++prop_verifyTraces :: Property+prop_verifyTraces =+ property $ do+ fetchedKeysVar <- liftIO $ newIORef []+ startedVar <- liftIO $ newIORef mempty+ tracesVar <- liftIO $ newIORef mempty+ let+ keyFetched k =+ atomicModifyIORef fetchedKeysVar $ \ks -> (k : ks, ())++ rules :: Int -> Rules Key+ rules input =+ memoise startedVar $+ verifyTraces+ tracesVar+ (\query value ->+ pure $ Const $ has' @Hashable @Identity query $ hash $ Identity value+ ) $+ withKeyFetchedCallback keyFetched $+ inputRules input++ nonDependentKey <- forAll $ Gen.filter (/= "dependent") string++ liftIO $ runTask (rules 1) $ do+ void $ fetch $ StringKey "dependent"+ void $ fetch $ StringKey nonDependentKey++ liftIO $ atomicWriteIORef startedVar mempty+ liftIO $ atomicWriteIORef fetchedKeysVar mempty++ liftIO $ runTask (rules 2) $ do+ void $ fetch $ StringKey "dependent"+ void $ fetch $ StringKey nonDependentKey++ fetchedKeys <- liftIO $ readIORef fetchedKeysVar+ sort fetchedKeys === [Some $ Writer $ IntKey 0, Some $ Writer $ StringKey "dependent"]++main :: IO ()+main =+ void $ checkParallel $$(discover)