packages feed

hls-graph 1.6.0.0 → 1.7.0.0

raw patch · 16 files changed

+549/−275 lines, 16 filesdep +hls-graphdep +hspecdep +tastydep ~asyncdep ~basenew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: hls-graph, hspec, tasty, tasty-hspec, tasty-hunit, tasty-rerun, text

Dependency ranges changed: async, base

API changes (from Hackage documentation)

- Development.IDE.Graph.Database: shakeOpenDatabase :: ShakeOptions -> Rules () -> IO (IO ShakeDatabase, IO ())
- Development.IDE.Graph.Internal.Database: instance GHC.Base.Functor Development.IDE.Graph.Internal.Database.Wait
- Development.IDE.Graph.Internal.Options: [shakeFiles] :: ShakeOptions -> FilePath
- Development.IDE.Graph.Internal.Options: [shakeThreads] :: ShakeOptions -> Int
+ Development.IDE.Graph.Database: shakeNewDatabase :: ShakeOptions -> Rules () -> IO ShakeDatabase
+ Development.IDE.Graph.Internal.Action: applyWithoutDependency :: (RuleResult key ~ value, ShakeValue key, Typeable value) => [key] -> Action [value]
+ Development.IDE.Graph.Internal.Types: GraphException :: String -> [String] -> e -> GraphException
+ Development.IDE.Graph.Internal.Types: ShakeDatabase :: !Int -> [Action ()] -> Database -> ShakeDatabase
+ Development.IDE.Graph.Internal.Types: Stack :: [Key] -> !HashSet Key -> Stack
+ Development.IDE.Graph.Internal.Types: StackException :: Stack -> StackException
+ Development.IDE.Graph.Internal.Types: [actionStack] :: SAction -> !Stack
+ Development.IDE.Graph.Internal.Types: [inner] :: GraphException -> e
+ Development.IDE.Graph.Internal.Types: [stack] :: GraphException -> [String]
+ Development.IDE.Graph.Internal.Types: [target] :: GraphException -> String
+ Development.IDE.Graph.Internal.Types: addStack :: Key -> Stack -> Either StackException Stack
+ Development.IDE.Graph.Internal.Types: data GraphException
+ Development.IDE.Graph.Internal.Types: data ShakeDatabase
+ Development.IDE.Graph.Internal.Types: data Stack
+ Development.IDE.Graph.Internal.Types: emptyStack :: Stack
+ Development.IDE.Graph.Internal.Types: fromGraphException :: Typeable b => SomeException -> Maybe b
+ Development.IDE.Graph.Internal.Types: instance GHC.Classes.Eq Development.IDE.Graph.Internal.Types.ResultDeps
+ Development.IDE.Graph.Internal.Types: instance GHC.Exception.Type.Exception Development.IDE.Graph.Internal.Types.GraphException
+ Development.IDE.Graph.Internal.Types: instance GHC.Exception.Type.Exception Development.IDE.Graph.Internal.Types.StackException
+ Development.IDE.Graph.Internal.Types: instance GHC.Show.Show Development.IDE.Graph.Internal.Types.GraphException
+ Development.IDE.Graph.Internal.Types: instance GHC.Show.Show Development.IDE.Graph.Internal.Types.ResultDeps
+ Development.IDE.Graph.Internal.Types: instance GHC.Show.Show Development.IDE.Graph.Internal.Types.Stack
+ Development.IDE.Graph.Internal.Types: instance GHC.Show.Show Development.IDE.Graph.Internal.Types.StackException
+ Development.IDE.Graph.Internal.Types: memberStack :: Key -> Stack -> Bool
+ Development.IDE.Graph.Internal.Types: newtype StackException
+ Development.IDE.Graph.Rule: applyWithoutDependency :: (RuleResult key ~ value, ShakeValue key, Typeable value) => [key] -> Action [value]
- Development.IDE.Graph.Database: shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO ([a], [IO ()])
+ Development.IDE.Graph.Database: shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO [a]
- Development.IDE.Graph.Database: shakeRunDatabaseForKeys :: Maybe [Key] -> ShakeDatabase -> [Action a] -> IO ([a], [IO ()])
+ Development.IDE.Graph.Database: shakeRunDatabaseForKeys :: Maybe [Key] -> ShakeDatabase -> [Action a] -> IO [a]
- Development.IDE.Graph.Internal.Database: build :: forall key value. (RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value) => Database -> [key] -> IO ([Key], [value])
+ Development.IDE.Graph.Internal.Database: build :: forall key value. (RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value) => Database -> Stack -> [key] -> IO ([Key], [value])
- Development.IDE.Graph.Internal.Options: ShakeOptions :: Int -> FilePath -> Maybe Dynamic -> Bool -> Bool -> ShakeOptions
+ Development.IDE.Graph.Internal.Options: ShakeOptions :: Maybe Dynamic -> Bool -> Bool -> ShakeOptions
- Development.IDE.Graph.Internal.Types: Clean :: Result -> Status
+ Development.IDE.Graph.Internal.Types: Clean :: !Result -> Status
- Development.IDE.Graph.Internal.Types: Result :: !Value -> !Step -> !Step -> !Step -> !ResultDeps -> !Seconds -> ByteString -> Result
+ Development.IDE.Graph.Internal.Types: Result :: !Value -> !Step -> !Step -> !Step -> !ResultDeps -> !Seconds -> !ByteString -> Result
- Development.IDE.Graph.Internal.Types: SAction :: !Database -> !IORef ResultDeps -> SAction
+ Development.IDE.Graph.Internal.Types: SAction :: !Database -> !IORef ResultDeps -> !Stack -> SAction
- Development.IDE.Graph.Internal.Types: [resultData] :: Result -> ByteString
+ Development.IDE.Graph.Internal.Types: [resultData] :: Result -> !ByteString

Files

hls-graph.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name:          hls-graph-version:       1.6.0.0+version:       1.7.0.0 synopsis:      Haskell Language Server internal graph API description:   Please see the README on GitHub at <https://github.com/haskell/haskell-language-server/tree/master/hls-graph#readme>@@ -104,3 +104,33 @@     DataKinds     KindSignatures     TypeOperators++test-suite tests+  type:             exitcode-stdio-1.0+  default-language: Haskell2010+  hs-source-dirs:   test+  main-is:          Main.hs+  other-modules:+    ActionSpec+    DatabaseSpec+    Example+    RulesSpec+    Spec++  ghc-options:      -threaded -rtsopts -with-rtsopts=-N -fno-ignore-asserts+  build-depends:+    , base+    , containers+    , directory+    , extra+    , filepath+    , hls-graph+    , hspec+    , stm+    , stm-containers+    , tasty+    , tasty-hspec+    , tasty-hunit+    , tasty-rerun+    , text+  build-tool-depends: hspec-discover:hspec-discover -any
src/Development/IDE/Graph.hs view
@@ -5,7 +5,7 @@     Key(..),     actionFinally, actionBracket, actionCatch, actionFork,     -- * Configuration-    ShakeOptions(shakeAllowRedefineRules, shakeThreads, shakeFiles, shakeExtra),+    ShakeOptions(shakeAllowRedefineRules, shakeExtra),     getShakeExtra, getShakeExtraRules, newShakeExtra,     -- * Explicit parallelism     parallel,
src/Development/IDE/Graph/Classes.hs view
@@ -1,8 +1,8 @@-
-module Development.IDE.Graph.Classes(
-    Show(..), Typeable, Eq(..), Hashable(..), NFData(..)
-    ) where
-
-import           Control.DeepSeq
-import           Data.Hashable
-import           Data.Typeable
++module Development.IDE.Graph.Classes(+    Show(..), Typeable, Eq(..), Hashable(..), NFData(..)+    ) where++import           Control.DeepSeq+import           Data.Hashable+import           Data.Typeable
src/Development/IDE/Graph/Database.hs view
@@ -4,7 +4,7 @@ module Development.IDE.Graph.Database(     ShakeDatabase,     ShakeValue,-    shakeOpenDatabase,+    shakeNewDatabase,     shakeRunDatabase,     shakeRunDatabaseForKeys,     shakeProfileDatabase,@@ -23,14 +23,10 @@ import           Development.IDE.Graph.Internal.Rules import           Development.IDE.Graph.Internal.Types -data ShakeDatabase = ShakeDatabase !Int [Action ()] Database  -- Placeholder to be the 'extra' if the user doesn't set it data NonExportedType = NonExportedType -shakeOpenDatabase :: ShakeOptions -> Rules () -> IO (IO ShakeDatabase, IO ())-shakeOpenDatabase opts rules = pure (shakeNewDatabase opts rules, pure ())- shakeNewDatabase :: ShakeOptions -> Rules () -> IO ShakeDatabase shakeNewDatabase opts rules = do     let extra = fromMaybe (toDyn NonExportedType) $ shakeExtra opts@@ -38,7 +34,7 @@     db <- newDatabase extra theRules     pure $ ShakeDatabase (length actions) actions db -shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO ([a], [IO ()])+shakeRunDatabase :: ShakeDatabase -> [Action a] -> IO [a] shakeRunDatabase = shakeRunDatabaseForKeys Nothing  -- | Returns the set of dirty keys annotated with their age (in # of builds)@@ -62,11 +58,10 @@       -- ^ Set of keys changed since last run. 'Nothing' means everything has changed     -> ShakeDatabase     -> [Action a]-    -> IO ([a], [IO ()])+    -> IO [a] shakeRunDatabaseForKeys keysChanged (ShakeDatabase lenAs1 as1 db) as2 = do     incDatabase db keysChanged-    as <- fmap (drop lenAs1) $ runActions db $ map unvoid as1 ++ as2-    return (as, [])+    fmap (drop lenAs1) $ runActions db $ map unvoid as1 ++ as2  -- | Given a 'ShakeDatabase', write an HTML profile to the given file about the latest run. shakeProfileDatabase :: ShakeDatabase -> FilePath -> IO ()
src/Development/IDE/Graph/Internal/Action.hs view
@@ -1,137 +1,147 @@-{-# LANGUAGE ConstraintKinds     #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies        #-}
-
-module Development.IDE.Graph.Internal.Action
-( ShakeValue
-, actionFork
-, actionBracket
-, actionCatch
-, actionFinally
-, alwaysRerun
-, apply1
-, apply
-, parallel
-, reschedule
-, runActions
-, Development.IDE.Graph.Internal.Action.getDirtySet
-, getKeysAndVisitedAge
-) where
-
-import           Control.Concurrent.Async
-import           Control.Exception
-import           Control.Monad.IO.Class
-import           Control.Monad.Trans.Class
-import           Control.Monad.Trans.Reader
-import           Data.IORef
-import           Development.IDE.Graph.Classes
-import           Development.IDE.Graph.Internal.Database
-import           Development.IDE.Graph.Internal.Rules    (RuleResult)
-import           Development.IDE.Graph.Internal.Types
-import           System.Exit
-
-type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, NFData a)
-
--- | Always rerun this rule when dirty, regardless of the dependencies.
-alwaysRerun :: Action ()
-alwaysRerun = do
-    ref <- Action $ asks actionDeps
-    liftIO $ modifyIORef ref (AlwaysRerunDeps [] <>)
-
--- No-op for now
-reschedule :: Double -> Action ()
-reschedule _ = pure ()
-
-parallel :: [Action a] -> Action [a]
-parallel [] = pure []
-parallel [x] = fmap (:[]) x
-parallel xs = do
-    a <- Action ask
-    deps <- liftIO $ readIORef $ actionDeps a
-    case deps of
-        UnknownDeps ->
-            -- if we are already in the rerun mode, nothing we do is going to impact our state
-            liftIO $ mapConcurrently (ignoreState a) xs
-        deps -> do
-            (newDeps, res) <- liftIO $ unzip <$> mapConcurrently (usingState a) xs
-            liftIO $ writeIORef (actionDeps a) $ mconcat $ deps : newDeps
-            pure res
-    where
-        usingState a x = do
-            ref <- newIORef mempty
-            res <- runReaderT (fromAction x) a{actionDeps=ref}
-            deps <- readIORef ref
-            pure (deps, res)
-
-ignoreState :: SAction -> Action b -> IO b
-ignoreState a x = do
-    ref <- newIORef mempty
-    runReaderT (fromAction x) a{actionDeps=ref}
-
-actionFork :: Action a -> (Async a -> Action b) -> Action b
-actionFork act k = do
-    a <- Action ask
-    deps <- liftIO $ readIORef $ actionDeps a
-    let db = actionDatabase a
-    case deps of
-        UnknownDeps -> do
-            -- if we are already in the rerun mode, nothing we do is going to impact our state
-            [res] <- liftIO $ withAsync (ignoreState a act) $ \as -> runActions db [k as]
-            return res
-        _ ->
-            error "please help me"
-
-isAsyncException :: SomeException -> Bool
-isAsyncException e
-    | Just (_ :: AsyncCancelled) <- fromException e = True
-    | Just (_ :: AsyncException) <- fromException e = True
-    | Just (_ :: ExitCode) <- fromException e = True
-    | otherwise = False
-
-
-actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a
-actionCatch a b = do
-    v <- Action ask
-    Action $ lift $ catchJust f (runReaderT (fromAction a) v) (\x -> runReaderT (fromAction (b x)) v)
-    where
-        -- Catch only catches exceptions that were caused by this code, not those that
-        -- are a result of program termination
-        f e | isAsyncException e = Nothing
-            | otherwise = fromException e
-
-actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c
-actionBracket a b c = do
-    v <- Action ask
-    Action $ lift $ bracket a b (\x -> runReaderT (fromAction (c x)) v)
-
-actionFinally :: Action a -> IO b -> Action a
-actionFinally a b = do
-    v <- Action ask
-    Action $ lift $ finally (runReaderT (fromAction a) v) b
-
-apply1 :: (RuleResult key ~ value, ShakeValue key, Typeable value) => key -> Action value
-apply1 k = head <$> apply [k]
-
-apply :: (RuleResult key ~ value, ShakeValue key, Typeable value) => [key] -> Action [value]
-apply ks = do
-    db <- Action $ asks actionDatabase
-    (is, vs) <- liftIO $ build db ks
-    ref <- Action $ asks actionDeps
-    liftIO $ modifyIORef ref (ResultDeps is <>)
-    pure vs
-
-runActions :: Database -> [Action a] -> IO [a]
-runActions db xs = do
-    deps <- newIORef mempty
-    runReaderT (fromAction $ parallel xs) $ SAction db deps
-
--- | Returns the set of dirty keys annotated with their age (in # of builds)
-getDirtySet  :: Action [(Key, Int)]
-getDirtySet = do
-    db <- getDatabase
-    liftIO $ Development.IDE.Graph.Internal.Database.getDirtySet db
-
-getKeysAndVisitedAge :: Action [(Key, Int)]
-getKeysAndVisitedAge = do
-    db <- getDatabase
-    liftIO $ Development.IDE.Graph.Internal.Database.getKeysAndVisitAge db
+{-# LANGUAGE ConstraintKinds     #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies        #-}++module Development.IDE.Graph.Internal.Action+( ShakeValue+, actionFork+, actionBracket+, actionCatch+, actionFinally+, alwaysRerun+, apply1+, apply+, applyWithoutDependency+, parallel+, reschedule+, runActions+, Development.IDE.Graph.Internal.Action.getDirtySet+, getKeysAndVisitedAge+) where++import           Control.Concurrent.Async+import           Control.Exception+import           Control.Monad.IO.Class+import           Control.Monad.Trans.Class+import           Control.Monad.Trans.Reader+import           Data.IORef+import           Development.IDE.Graph.Classes+import           Development.IDE.Graph.Internal.Database+import           Development.IDE.Graph.Internal.Rules    (RuleResult)+import           Development.IDE.Graph.Internal.Types+import           System.Exit++type ShakeValue a = (Show a, Typeable a, Eq a, Hashable a, NFData a)++-- | Always rerun this rule when dirty, regardless of the dependencies.+alwaysRerun :: Action ()+alwaysRerun = do+    ref <- Action $ asks actionDeps+    liftIO $ modifyIORef ref (AlwaysRerunDeps [] <>)++-- No-op for now+reschedule :: Double -> Action ()+reschedule _ = pure ()++parallel :: [Action a] -> Action [a]+parallel [] = pure []+parallel [x] = fmap (:[]) x+parallel xs = do+    a <- Action ask+    deps <- liftIO $ readIORef $ actionDeps a+    case deps of+        UnknownDeps ->+            -- if we are already in the rerun mode, nothing we do is going to impact our state+            liftIO $ mapConcurrently (ignoreState a) xs+        deps -> do+            (newDeps, res) <- liftIO $ unzip <$> mapConcurrently (usingState a) xs+            liftIO $ writeIORef (actionDeps a) $ mconcat $ deps : newDeps+            pure res+    where+        usingState a x = do+            ref <- newIORef mempty+            res <- runReaderT (fromAction x) a{actionDeps=ref}+            deps <- readIORef ref+            pure (deps, res)++ignoreState :: SAction -> Action b -> IO b+ignoreState a x = do+    ref <- newIORef mempty+    runReaderT (fromAction x) a{actionDeps=ref}++actionFork :: Action a -> (Async a -> Action b) -> Action b+actionFork act k = do+    a <- Action ask+    deps <- liftIO $ readIORef $ actionDeps a+    let db = actionDatabase a+    case deps of+        UnknownDeps -> do+            -- if we are already in the rerun mode, nothing we do is going to impact our state+            [res] <- liftIO $ withAsync (ignoreState a act) $ \as -> runActions db [k as]+            return res+        _ ->+            error "please help me"++isAsyncException :: SomeException -> Bool+isAsyncException e+    | Just (_ :: AsyncCancelled) <- fromException e = True+    | Just (_ :: AsyncException) <- fromException e = True+    | Just (_ :: ExitCode) <- fromException e = True+    | otherwise = False+++actionCatch :: Exception e => Action a -> (e -> Action a) -> Action a+actionCatch a b = do+    v <- Action ask+    Action $ lift $ catchJust f (runReaderT (fromAction a) v) (\x -> runReaderT (fromAction (b x)) v)+    where+        -- Catch only catches exceptions that were caused by this code, not those that+        -- are a result of program termination+        f e | isAsyncException e = Nothing+            | otherwise = fromException e++actionBracket :: IO a -> (a -> IO b) -> (a -> Action c) -> Action c+actionBracket a b c = do+    v <- Action ask+    Action $ lift $ bracket a b (\x -> runReaderT (fromAction (c x)) v)++actionFinally :: Action a -> IO b -> Action a+actionFinally a b = do+    v <- Action ask+    Action $ lift $ finally (runReaderT (fromAction a) v) b++apply1 :: (RuleResult key ~ value, ShakeValue key, Typeable value) => key -> Action value+apply1 k = head <$> apply [k]++apply :: (RuleResult key ~ value, ShakeValue key, Typeable value) => [key] -> Action [value]+apply ks = do+    db <- Action $ asks actionDatabase+    stack <- Action $ asks actionStack+    (is, vs) <- liftIO $ build db stack ks+    ref <- Action $ asks actionDeps+    liftIO $ modifyIORef ref (ResultDeps is <>)+    pure vs++-- | Evaluate a list of keys without recording any dependencies.+applyWithoutDependency :: (RuleResult key ~ value, ShakeValue key, Typeable value) => [key] -> Action [value]+applyWithoutDependency ks = do+    db <- Action $ asks actionDatabase+    stack <- Action $ asks actionStack+    (_, vs) <- liftIO $ build db stack ks+    pure vs++runActions :: Database -> [Action a] -> IO [a]+runActions db xs = do+    deps <- newIORef mempty+    runReaderT (fromAction $ parallel xs) $ SAction db deps emptyStack++-- | Returns the set of dirty keys annotated with their age (in # of builds)+getDirtySet  :: Action [(Key, Int)]+getDirtySet = do+    db <- getDatabase+    liftIO $ Development.IDE.Graph.Internal.Database.getDirtySet db++getKeysAndVisitedAge :: Action [(Key, Int)]+getKeysAndVisitedAge = do+    db <- getDatabase+    liftIO $ Development.IDE.Graph.Internal.Database.getKeysAndVisitAge db
src/Development/IDE/Graph/Internal/Database.hs view
@@ -8,6 +8,7 @@ {-# LANGUAGE RecordWildCards            #-} {-# LANGUAGE ScopedTypeVariables        #-} {-# LANGUAGE TypeFamilies               #-}+{-# LANGUAGE TupleSections #-}  module Development.IDE.Graph.Internal.Database (newDatabase, incDatabase, build, getDirtySet, getKeysAndVisitAge) where @@ -32,14 +33,15 @@ import           Data.Maybe import           Data.Traversable                     (for) import           Data.Tuple.Extra+import           Debug.Trace (traceM) import           Development.IDE.Graph.Classes import           Development.IDE.Graph.Internal.Rules import           Development.IDE.Graph.Internal.Types import qualified Focus import qualified ListT import qualified StmContainers.Map                    as SMap+import           System.Time.Extra                    (duration, sleep) import           System.IO.Unsafe-import           System.Time.Extra                    (duration)  newDatabase :: Dynamic -> TheRules -> IO Database newDatabase databaseExtra databaseRules = do@@ -77,10 +79,11 @@ -- | Unwrap and build a list of keys in parallel build     :: forall key value . (RuleResult key ~ value, Typeable key, Show key, Hashable key, Eq key, Typeable value)-    => Database -> [key] -> IO ([Key], [value])-build db keys = do+    => Database -> Stack -> [key] -> IO ([Key], [value])+-- build _ st k | traceShow ("build", st, k) False = undefined+build db stack keys = do     (ids, vs) <- runAIO $ fmap unzip $ either return liftIO =<<-            builder db (map Key keys)+            builder db stack (map Key keys)     pure (ids, map (asV . resultValue) vs)     where         asV :: Value -> value@@ -90,8 +93,9 @@ --  If none of the keys are dirty, we can return the results immediately. --  Otherwise, a blocking computation is returned *which must be evaluated asynchronously* to avoid deadlock. builder-    :: Database -> [Key] -> AIO (Either [(Key, Result)] (IO [(Key, Result)]))-builder db@Database{..} keys = withRunInIO $ \(RunInIO run) -> do+    :: Database -> Stack -> [Key] -> AIO (Either [(Key, Result)] (IO [(Key, Result)]))+-- builder _ st kk | traceShow ("builder", st,kk) False = undefined+builder db@Database{..} stack keys = withRunInIO $ \(RunInIO run) -> do     -- Things that I need to force before my results are ready     toForce <- liftIO $ newTVarIO []     current <- liftIO $ readTVarIO databaseStep@@ -103,11 +107,13 @@             status <- SMap.lookup id databaseValues             val <- case viewDirty current $ maybe (Dirty Nothing) keyStatus status of                 Clean r -> pure r-                Running _ force val _ -> do+                Running _ force val _+                  | memberStack id stack -> throw $ StackException stack+                  | otherwise -> do                     modifyTVar' toForce (Wait force :)                     pure val                 Dirty s -> do-                    let act = run (refresh db id s)+                    let act = run (refresh db stack id s)                         (force, val) = splitIO (join act)                     SMap.focus (updateStatus $ Running current force val s) id databaseValues                     modifyTVar' toForce (Spawn force:)@@ -116,7 +122,7 @@             pure (id, val)      toForceList <- liftIO $ readTVarIO toForce-    let waitAll = run $ mapConcurrentlyAIO_ id toForceList+    let waitAll = run $ waitConcurrently_ toForceList     case toForceList of         [] -> return $ Left results         _ -> return $ Right $ do@@ -127,32 +133,33 @@ --     * If no dirty dependencies and we have evaluated the key previously, then we refresh it in the current thread. --       This assumes that the implementation will be a lookup --     * Otherwise, we spawn a new thread to refresh the dirty deps (if any) and the key itself-refresh :: Database -> Key -> Maybe Result -> AIO (IO Result)-refresh db key result@(Just me@Result{resultDeps = ResultDeps deps}) = do-    res <- builder db deps-    case res of-      Left res ->-        if isDirty res-            then asyncWithCleanUp $ liftIO $ compute db key RunDependenciesChanged result-            else pure $ compute db key RunDependenciesSame result-      Right iores -> asyncWithCleanUp $ liftIO $ do-        res <- iores-        let mode = if isDirty res then RunDependenciesChanged else RunDependenciesSame-        compute db key mode result-    where-        isDirty = any (\(_,dep) -> resultBuilt me < resultChanged dep)--refresh db key result =-    asyncWithCleanUp $ liftIO $ compute db key RunDependenciesChanged result-+refresh :: Database -> Stack -> Key -> Maybe Result -> AIO (IO Result)+-- refresh _ st k _ | traceShow ("refresh", st, k) False = undefined+refresh db stack key result = case (addStack key stack, result) of+    (Left e, _) -> throw e+    (Right stack, Just me@Result{resultDeps = ResultDeps deps}) -> do+        res <- builder db stack deps+        let isDirty = any (\(_,dep) -> resultBuilt me < resultChanged dep)+        case res of+            Left res ->+                if isDirty res+                    then asyncWithCleanUp $ liftIO $ compute db stack key RunDependenciesChanged result+                    else pure $ compute db stack key RunDependenciesSame result+            Right iores -> asyncWithCleanUp $ liftIO $ do+                res <- iores+                let mode = if isDirty res then RunDependenciesChanged else RunDependenciesSame+                compute db stack key mode result+    (Right stack, _) ->+        asyncWithCleanUp $ liftIO $ compute db stack key RunDependenciesChanged result  -- | Compute a key.-compute :: Database -> Key -> RunMode -> Maybe Result -> IO Result-compute db@Database{..} key mode result = do+compute :: Database -> Stack -> Key -> RunMode -> Maybe Result -> IO Result+-- compute _ st k _ _ | traceShow ("compute", st, k) False = undefined+compute db@Database{..} stack key mode result = do     let act = runRule databaseRules key (fmap resultData result) mode     deps <- newIORef UnknownDeps     (execution, RunResult{..}) <--        duration $ runReaderT (fromAction act) $ SAction db deps+        duration $ runReaderT (fromAction act) $ SAction db deps stack     built <- readTVarIO databaseStep     deps <- readIORef deps     let changed = if runChanged == ChangedRecomputeDiff then built else maybe built resultChanged result@@ -165,7 +172,11 @@         deps | not(null deps)             && runChanged /= ChangedNothing                     -> do-            void $ forkIO $+            -- IMPORTANT: record the reverse deps **before** marking the key Clean.+            -- If an async exception strikes before the deps have been recorded,+            -- we won't be able to accurately propagate dirtiness for this key+            -- on the next build.+            void $                 updateReverseDeps key db                     (getResultDepsDefault [] previousDeps)                     (HSet.fromList deps)@@ -219,7 +230,8 @@     -> [Key] -- ^ Previous direct dependencies of Id     -> HashSet Key -- ^ Current direct dependencies of Id     -> IO ()-updateReverseDeps myId db prev new = uninterruptibleMask_ $ do+-- mask to ensure that all the reverse dependencies are updated+updateReverseDeps myId db prev new = do     forM_ prev $ \d ->         unless (d `HSet.member` new) $             doOne (HSet.delete myId) d@@ -247,20 +259,27 @@             next <- lift $ atomically $ getReverseDependencies database x             traverse_ loop (maybe mempty HSet.toList next) --- | IO extended to track created asyncs to clean them up when the thread is killed,---   generalizing 'withAsync'+--------------------------------------------------------------------------------+-- Asynchronous computations with cancellation++-- | A simple monad to implement cancellation on top of 'Async',+--   generalizing 'withAsync' to monadic scopes. newtype AIO a = AIO { unAIO :: ReaderT (IORef [Async ()]) IO a }   deriving newtype (Applicative, Functor, Monad, MonadIO) +-- | Run the monadic computation, cancelling all the spawned asyncs if an exception arises runAIO :: AIO a -> IO a runAIO (AIO act) = do     asyncs <- newIORef []     runReaderT act asyncs `onException` cleanupAsync asyncs +-- | Like 'async' but with built-in cancellation.+--   Returns an IO action to wait on the result. asyncWithCleanUp :: AIO a -> AIO (IO a) asyncWithCleanUp act = do     st <- AIO ask     io <- unliftAIO act+    -- mask to make sure we keep track of the spawned async     liftIO $ uninterruptibleMask $ \restore -> do         a <- async $ restore io         atomicModifyIORef'_ st (void a :)@@ -279,28 +298,45 @@     k $ RunInIO (\aio -> runReaderT (unAIO aio) st)  cleanupAsync :: IORef [Async a] -> IO ()-cleanupAsync ref = uninterruptibleMask_ $ do-    asyncs <- readIORef ref+-- mask to make sure we interrupt all the asyncs+cleanupAsync ref = uninterruptibleMask $ \unmask -> do+    asyncs <- atomicModifyIORef' ref ([],)+    -- interrupt all the asyncs without waiting     mapM_ (\a -> throwTo (asyncThreadId a) AsyncCancelled) asyncs-    mapM_ waitCatch asyncs+    -- Wait until all the asyncs are done+    -- But if it takes more than 10 seconds, log to stderr+    unless (null asyncs) $ do+        let warnIfTakingTooLong = unmask $ forever $ do+                sleep 10+                traceM "cleanupAsync: waiting for asyncs to finish"+        withAsync warnIfTakingTooLong $ \_ ->+            mapM_ waitCatch asyncs -data Wait a-    = Wait {justWait :: !a}-    | Spawn {justWait :: !a}-    deriving Functor+data Wait+    = Wait {justWait :: !(IO ())}+    | Spawn {justWait :: !(IO ())} -waitOrSpawn :: Wait (IO a) -> IO (Either (IO a) (Async a))+fmapWait :: (IO () -> IO ()) -> Wait -> Wait+fmapWait f (Wait io) = Wait (f io)+fmapWait f (Spawn io) = Spawn (f io)++waitOrSpawn :: Wait -> IO (Either (IO ()) (Async ())) waitOrSpawn (Wait io)  = pure $ Left io waitOrSpawn (Spawn io) = Right <$> async io -mapConcurrentlyAIO_ :: (a -> IO ()) -> [Wait a] -> AIO ()-mapConcurrentlyAIO_ _ [] = pure ()-mapConcurrentlyAIO_ f [one] = liftIO $ justWait $ fmap f one-mapConcurrentlyAIO_ f many = do+waitConcurrently_ :: [Wait] -> AIO ()+waitConcurrently_ [] = pure ()+waitConcurrently_ [one] = liftIO $ justWait one+waitConcurrently_ many = do     ref <- AIO ask-    waits <- liftIO $ uninterruptibleMask $ \restore -> do-        waits <- liftIO $ traverse (waitOrSpawn . fmap (restore . f)) many-        let asyncs = rights waits+    -- spawn the async computations.+    -- mask to make sure we keep track of all the asyncs.+    (asyncs, syncs) <- liftIO $ uninterruptibleMask $ \unmask -> do+        waits <- liftIO $ traverse (waitOrSpawn . fmapWait unmask) many+        let (syncs, asyncs) = partitionEithers waits         liftIO $ atomicModifyIORef'_ ref (asyncs ++)-        return waits-    liftIO $ traverse_ (either id wait) waits+        return (asyncs, syncs)+    -- work on the sync computations+    liftIO $ sequence_ syncs+    -- wait for the async computations before returning+    liftIO $ traverse_ wait asyncs
src/Development/IDE/Graph/Internal/Options.hs view
@@ -5,16 +5,13 @@ import           Development.IDE.Graph.Internal.Types  data ShakeOptions = ShakeOptions {-    -- | Has no effect, kept only for api compatibility with Shake-    shakeThreads            :: Int,-    shakeFiles              :: FilePath,     shakeExtra              :: Maybe Dynamic,     shakeAllowRedefineRules :: Bool,     shakeTimings            :: Bool     }  shakeOptions :: ShakeOptions-shakeOptions = ShakeOptions 0 ".shake" Nothing False False+shakeOptions = ShakeOptions Nothing False False  getShakeExtra :: Typeable a => Action (Maybe a) getShakeExtra = do
src/Development/IDE/Graph/Internal/Rules.hs view
@@ -1,58 +1,57 @@--- We deliberately want to ensure the function we add to the rule database
--- has the constraints we need on it when we get it out.
-{-# OPTIONS_GHC -Wno-redundant-constraints #-}
-{-# LANGUAGE RecordWildCards     #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies        #-}
-
-module Development.IDE.Graph.Internal.Rules where
-
-import           Control.Exception.Extra
-import           Control.Monad
-import           Control.Monad.IO.Class
-import           Control.Monad.Trans.Reader
-import qualified Data.ByteString                      as BS
-import           Data.Dynamic
-import qualified Data.HashMap.Strict                  as Map
-import           Data.IORef
-import           Data.Maybe
-import           Data.Typeable
-import           Development.IDE.Graph.Classes
-import           Development.IDE.Graph.Internal.Types
-
--- | The type mapping between the @key@ or a rule and the resulting @value@.
---   See 'addBuiltinRule' and 'Development.Shake.Rule.apply'.
-type family RuleResult key -- = value
-
-action :: Action a -> Rules ()
-action x = do
-    ref <- Rules $ asks rulesActions
-    liftIO $ modifyIORef' ref (void x:)
-
-addRule
-    :: forall key value .
-       (RuleResult key ~ value, Typeable key, Hashable key, Eq key, Typeable value)
-    => (key -> Maybe BS.ByteString -> RunMode -> Action (RunResult value))
-    -> Rules ()
-addRule f = do
-    ref <- Rules $ asks rulesMap
-    liftIO $ modifyIORef' ref $ Map.insert (typeRep (Proxy :: Proxy key)) (toDyn f2)
-    where
-        f2 :: Key -> Maybe BS.ByteString -> RunMode -> Action (RunResult Value)
-        f2 (Key a) b c = do
-            v <- f (fromJust $ cast a :: key) b c
-            v <- liftIO $ evaluate v
-            pure $ Value . toDyn <$> v
-
-runRule
-    :: TheRules -> Key -> Maybe BS.ByteString -> RunMode -> Action (RunResult Value)
-runRule rules key@(Key t) bs mode = case Map.lookup (typeOf t) rules of
-    Nothing -> liftIO $ errorIO "Could not find key"
-    Just x  -> unwrapDynamic x key bs mode
-
-runRules :: Dynamic -> Rules () -> IO (TheRules, [Action ()])
-runRules rulesExtra (Rules rules) = do
-    rulesActions <- newIORef []
-    rulesMap <- newIORef Map.empty
-    runReaderT rules SRules{..}
-    (,) <$> readIORef rulesMap <*> readIORef rulesActions
+-- We deliberately want to ensure the function we add to the rule database+-- has the constraints we need on it when we get it out.+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies        #-}++module Development.IDE.Graph.Internal.Rules where++import           Control.Exception.Extra+import           Control.Monad+import           Control.Monad.IO.Class+import           Control.Monad.Trans.Reader+import qualified Data.ByteString                      as BS+import           Data.Dynamic+import qualified Data.HashMap.Strict                  as Map+import           Data.IORef+import           Data.Maybe+import           Data.Typeable+import           Development.IDE.Graph.Classes+import           Development.IDE.Graph.Internal.Types++-- | The type mapping between the @key@ or a rule and the resulting @value@.+type family RuleResult key -- = value++action :: Action a -> Rules ()+action x = do+    ref <- Rules $ asks rulesActions+    liftIO $ modifyIORef' ref (void x:)++addRule+    :: forall key value .+       (RuleResult key ~ value, Typeable key, Hashable key, Eq key, Typeable value)+    => (key -> Maybe BS.ByteString -> RunMode -> Action (RunResult value))+    -> Rules ()+addRule f = do+    ref <- Rules $ asks rulesMap+    liftIO $ modifyIORef' ref $ Map.insert (typeRep (Proxy :: Proxy key)) (toDyn f2)+    where+        f2 :: Key -> Maybe BS.ByteString -> RunMode -> Action (RunResult Value)+        f2 (Key a) b c = do+            v <- f (fromJust $ cast a :: key) b c+            v <- liftIO $ evaluate v+            pure $ Value . toDyn <$> v++runRule+    :: TheRules -> Key -> Maybe BS.ByteString -> RunMode -> Action (RunResult Value)+runRule rules key@(Key t) bs mode = case Map.lookup (typeOf t) rules of+    Nothing -> liftIO $ errorIO "Could not find key"+    Just x  -> unwrapDynamic x key bs mode++runRules :: Dynamic -> Rules () -> IO (TheRules, [Action ()])+runRules rulesExtra (Rules rules) = do+    rulesActions <- newIORef []+    rulesMap <- newIORef Map.empty+    runReaderT rules SRules{..}+    (,) <$> readIORef rulesMap <*> readIORef rulesActions
src/Development/IDE/Graph/Internal/Types.hs view
@@ -26,7 +26,7 @@ import qualified Data.ByteString               as BS import           Data.Dynamic import qualified Data.HashMap.Strict           as Map-import           Data.HashSet                  (HashSet)+import           Data.HashSet                  (HashSet, member) import           Data.IORef import           Data.Maybe import           Data.Typeable@@ -36,6 +36,8 @@ import           StmContainers.Map             (Map) import qualified StmContainers.Map             as SMap import           System.Time.Extra             (Seconds)+import qualified Data.HashSet as Set+import Data.List (intercalate)   unwrapDynamic :: forall a . Typeable a => Dynamic -> a@@ -66,7 +68,8 @@  data SAction = SAction {     actionDatabase :: !Database,-    actionDeps     :: !(IORef ResultDeps)+    actionDeps     :: !(IORef ResultDeps),+    actionStack    :: !Stack     }  getDatabase :: Action Database@@ -75,6 +78,8 @@ --------------------------------------------------------------------- -- DATABASE +data ShakeDatabase = ShakeDatabase !Int [Action ()] Database+ newtype Step = Step Int     deriving newtype (Eq,Ord,Hashable) @@ -115,12 +120,12 @@                   . databaseValues  data Status-    = Clean Result+    = Clean !Result     | Dirty (Maybe Result)     | Running {         runningStep   :: !Step,         runningWait   :: !(IO ()),-        runningResult :: Result,+        runningResult :: Result,     -- LAZY         runningPrev   :: !(Maybe Result)         } @@ -140,10 +145,11 @@     resultVisited   :: !Step, -- ^ the step when it was last looked up     resultDeps      :: !ResultDeps,     resultExecution :: !Seconds, -- ^ How long it took, last time it ran-    resultData      :: BS.ByteString+    resultData      :: !BS.ByteString     }  data ResultDeps = UnknownDeps | AlwaysRerunDeps ![Key] | ResultDeps ![Key]+  deriving (Eq, Show)  getResultDepsDefault :: [Key] -> ResultDeps -> [Key] getResultDepsDefault _ (ResultDeps ids)      = ids@@ -200,6 +206,54 @@ instance NFData value => NFData (RunResult value) where     rnf (RunResult x1 x2 x3) = rnf x1 `seq` x2 `seq` rnf x3 +---------------------------------------------------------------------+-- EXCEPTIONS++data GraphException = forall e. Exception e => GraphException {+    target :: String, -- ^ The key that was being built+    stack  :: [String], -- ^ The stack of keys that led to this exception+    inner  :: e -- ^ The underlying exception+}+  deriving (Typeable, Exception)++instance Show GraphException where+    show GraphException{..} = unlines $+        ["GraphException: " ++ target] +++        stack +++        ["Inner exception: " ++ show inner]++fromGraphException :: Typeable b => SomeException -> Maybe b+fromGraphException x = do+    GraphException _ _ e <- fromException x+    cast e++---------------------------------------------------------------------+-- CALL STACK++data Stack = Stack [Key] !(HashSet Key)++instance Show Stack where+    show (Stack kk _) = "Stack: " <> intercalate " -> " (map show kk)++newtype StackException = StackException Stack+  deriving (Typeable, Show)++instance Exception StackException where+    fromException = fromGraphException+    toException this@(StackException (Stack stack _)) = toException $+        GraphException (show$ last stack) (map show stack) this++addStack :: Key -> Stack -> Either StackException Stack+addStack k (Stack ks is)+    | k `member` is = Left $ StackException stack2+    | otherwise = Right stack2+    where stack2 = Stack (k:ks) (Set.insert k is)++memberStack :: Key -> Stack -> Bool+memberStack k (Stack _ ks) = k `member` ks++emptyStack :: Stack+emptyStack = Stack [] mempty --------------------------------------------------------------------- -- INSTANCES 
src/Development/IDE/Graph/Rule.hs view
@@ -7,7 +7,7 @@     RunMode(..), RunChanged(..), RunResult(..),     -- * Calling builtin rules     -- | Wrappers around calling Shake rules. In general these should be specialised to a builtin rule.-    apply, apply1,+    apply, apply1, applyWithoutDependency     ) where  import           Development.IDE.Graph.Internal.Action
+ test/ActionSpec.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}++module ActionSpec where++import Control.Concurrent.STM+import Development.IDE.Graph (shakeOptions)+import Development.IDE.Graph.Database (shakeNewDatabase, shakeRunDatabase)+import Development.IDE.Graph.Internal.Action (apply1)+import Development.IDE.Graph.Internal.Types+import Development.IDE.Graph.Rule+import Example+import qualified StmContainers.Map as STM+import Test.Hspec+import System.Time.Extra (timeout)++spec :: Spec+spec = do+  describe "apply1" $ do+    it "computes a rule with no dependencies" $ do+      db <- shakeNewDatabase shakeOptions $ do+        ruleUnit+      res <- shakeRunDatabase db $+        pure $ do+          apply1 (Rule @())+      res `shouldBe` [()]+    it "computes a rule with one dependency" $ do+      db <- shakeNewDatabase shakeOptions $ do+        ruleUnit+        ruleBool+      res <- shakeRunDatabase db $ pure $ apply1 Rule+      res `shouldBe` [True]+    it "tracks direct dependencies" $ do+      db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do+        ruleUnit+        ruleBool+      let theKey = Rule @Bool+      res <- shakeRunDatabase db $+        pure $ do+          apply1 theKey+      res `shouldBe` [True]+      Just (Clean res) <- lookup (Key theKey) <$> getDatabaseValues theDb+      resultDeps res `shouldBe` ResultDeps [Key (Rule @())]+    it "tracks reverse dependencies" $ do+      db@(ShakeDatabase _ _ Database {..}) <- shakeNewDatabase shakeOptions $ do+        ruleUnit+        ruleBool+      let theKey = Rule @Bool+      res <- shakeRunDatabase db $+        pure $ do+          apply1 theKey+      res `shouldBe` [True]+      Just KeyDetails {..} <- atomically $ STM.lookup (Key (Rule @())) databaseValues+      keyReverseDeps `shouldBe` [Key theKey]+    it "rethrows exceptions" $ do+      db <- shakeNewDatabase shakeOptions $ do+        addRule $ \(Rule :: Rule ()) old mode -> error "boom"+      let res = shakeRunDatabase db $ pure $ apply1 (Rule @())+      res `shouldThrow` anyErrorCall+  describe "applyWithoutDependency" $ do+    it "does not track dependencies" $ do+      db@(ShakeDatabase _ _ theDb) <- shakeNewDatabase shakeOptions $ do+        ruleUnit+        addRule $ \Rule old mode -> do+            [()] <- applyWithoutDependency [Rule]+            return $ RunResult ChangedRecomputeDiff "" True++      let theKey = Rule @Bool+      res <- shakeRunDatabase db $+        pure $ do+          applyWithoutDependency [theKey]+      res `shouldBe` [[True]]+      Just (Clean res) <- lookup (Key theKey) <$> getDatabaseValues theDb+      resultDeps res `shouldBe` UnknownDeps
+ test/DatabaseSpec.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+module DatabaseSpec where++import Control.Concurrent.STM+import Development.IDE.Graph (shakeOptions)+import Development.IDE.Graph.Database (shakeNewDatabase, shakeRunDatabase)+import Development.IDE.Graph.Internal.Action (apply1)+import Development.IDE.Graph.Internal.Types+import Development.IDE.Graph.Rule+import Example+import qualified StmContainers.Map as STM+import Test.Hspec+import System.Time.Extra (timeout)++spec :: Spec+spec = do+    describe "Evaluation" $ do+        it "detects cycles" $ do+            db <- shakeNewDatabase shakeOptions $ do+                ruleBool+                addRule $ \Rule old mode -> do+                    True <- apply1 (Rule @Bool)+                    return $ RunResult ChangedRecomputeDiff "" ()+            let res = shakeRunDatabase db $ pure $ apply1 (Rule @())+            timeout 1 res `shouldThrow` \StackException{} -> True
+ test/Example.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+module Example where++import Development.IDE.Graph+import Development.IDE.Graph.Rule+import Development.IDE.Graph.Classes+import GHC.Generics+import Type.Reflection (typeRep)++data Rule a = Rule+    deriving (Eq, Generic, Hashable, NFData)++instance Typeable a => Show (Rule a) where+    show Rule = show $ typeRep @a++type instance RuleResult (Rule a) = a++ruleUnit :: Rules ()+ruleUnit = addRule $ \(Rule :: Rule ()) old mode -> do+    return $ RunResult ChangedRecomputeDiff "" ()++-- | Depends on Rule @()+ruleBool :: Rules ()+ruleBool = addRule $ \Rule old mode -> do+    () <- apply1 Rule+    return $ RunResult ChangedRecomputeDiff "" True
+ test/Main.hs view
@@ -0,0 +1,7 @@+import qualified Spec+import Test.Tasty+import Test.Tasty.Hspec+import Test.Tasty.Ingredients.Rerun (defaultMainWithRerun)++main :: IO ()+main = testSpecs Spec.spec >>= defaultMainWithRerun . testGroup "tactics"
+ test/RulesSpec.hs view
@@ -0,0 +1,8 @@+module RulesSpec where++import Test.Hspec++spec :: Spec+spec = do+    describe "" $ do+        pure ()
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}