module Hakyll.Core.DependencyAnalyzer ( DependencyAnalyzer (..) , Signal (..) , makeDependencyAnalyzer , step , stepAll ) where import Prelude hiding (reverse) import qualified Prelude as P (reverse) import Control.Arrow (first) import Data.Set (Set) import qualified Data.Set as S import Data.Monoid (Monoid, mappend, mempty) import Hakyll.Core.DirectedGraph -- | This data structure represents the state of the dependency analyzer. It -- holds a complete graph in 'analyzerGraph', which always contains all items, -- whether they are to be compiled or not. -- -- The 'analyzerRemains' fields holds the items that still need to be compiled, -- and 'analyzerDone' holds the items which are already compiled. This means -- that initally, 'analyzerDone' is empty and 'analyzerRemains' contains the -- items which are out-of-date (or items which have out-of-date dependencies). -- -- We also hold the dependency graph from the previous run because we need it -- when we want to determine when an item is out-of-date. An item is out-of-date -- when: -- -- * the resource from which it compiles is out-of-date, or; -- -- * any of it's dependencies is out-of-date, or; -- -- * it's set of dependencies has changed since the previous run. -- data DependencyAnalyzer a = DependencyAnalyzer { -- | The complete dependency graph analyzerGraph :: DirectedGraph a , -- | A set of items yet to be compiled analyzerRemains :: Set a , -- | A set of items already compiled analyzerDone :: Set a , -- | The dependency graph from the previous run analyzerPreviousGraph :: DirectedGraph a } deriving (Show) data Signal a = Build a | Cycle [a] | Done deriving (Show) instance (Ord a, Show a) => Monoid (DependencyAnalyzer a) where mempty = DependencyAnalyzer mempty mempty mempty mempty mappend x y = growRemains $ DependencyAnalyzer (analyzerGraph x `mappend` analyzerGraph y) (analyzerRemains x `mappend` analyzerRemains y) (analyzerDone x `mappend` analyzerDone y) (analyzerPreviousGraph x `mappend` analyzerPreviousGraph y) -- | Construct a dependency analyzer -- makeDependencyAnalyzer :: (Ord a, Show a) => DirectedGraph a -- ^ The dependency graph -> (a -> Bool) -- ^ Is an item out-of-date? -> DirectedGraph a -- ^ The old dependency graph -> DependencyAnalyzer a -- ^ Resulting analyzer makeDependencyAnalyzer graph isOutOfDate prev = growRemains $ DependencyAnalyzer graph remains S.empty prev where -- Construct the remains set by filtering using the given predicate remains = S.fromList $ filter isOutOfDate $ map fst $ toList graph -- | The 'analyzerRemains' field of a 'DependencyAnalyzer' is supposed to -- contain all out-of-date items, including the items with out-of-date -- dependencies. However, it is easier to just set up the directly out-of-date -- items initially -- and then grow the remains fields. -- -- This function assumes the 'analyzerRemains' fields in incomplete, and tries -- to correct it. Running it when the field is complete has no effect -- but it -- is a pretty expensive function, and it should be used with care. -- growRemains :: (Ord a, Show a) => DependencyAnalyzer a -> DependencyAnalyzer a growRemains (DependencyAnalyzer graph remains done prev) = (DependencyAnalyzer graph remains' done prev) where -- Grow the remains set using the indirect and changedDeps values, then -- filter out the items already done remains' = S.filter (`S.notMember` done) indirect -- Select the nodes which are reachable from the remaining nodes in the -- reversed dependency graph: these are the indirectly out-of-date items indirect = reachableNodes (remains `S.union` changedDeps) $ reverse graph -- For all nodes in the graph, check which items have a different dependency -- set compared to the previous run changedDeps = S.fromList $ map fst $ filter (uncurry (/=) . first (`neighbours` prev)) $ toList graph -- | Step a dependency analyzer -- step :: (Ord a, Show a) => DependencyAnalyzer a -> (Signal a, DependencyAnalyzer a) step analyzer@(DependencyAnalyzer graph remains done prev) -- No remaining items | S.null remains = (Done, analyzer) -- An item remains, let's find a ready item | otherwise = let item = S.findMin remains in case findReady analyzer item of Done -> (Done, analyzer) Cycle c -> (Cycle c, analyzer) -- A ready item was found, signal a build Build build -> let remains' = S.delete build remains done' = S.insert build done in (Build build, DependencyAnalyzer graph remains' done' prev) -- | Step until done, creating a set of items we need to build -- mostly used -- for debugging purposes -- stepAll :: (Ord a, Show a) => DependencyAnalyzer a -> Maybe (Set a) stepAll = stepAll' S.empty where stepAll' xs analyzer = case step analyzer of (Build x, analyzer') -> stepAll' (S.insert x xs) analyzer' (Done, _) -> Just xs (Cycle _, _) -> Nothing -- | Find an item ready to be compiled -- findReady :: (Ord a, Show a) => DependencyAnalyzer a -> a -> Signal a findReady analyzer = findReady' [] S.empty where -- The dependency graph graph = analyzerGraph analyzer -- Items to do todo = analyzerRemains analyzer `S.difference` analyzerDone analyzer -- Worker findReady' stack visited item -- We already visited this item, the cycle is the reversed stack | item `S.member` visited = Cycle $ P.reverse stack' -- Look at the neighbours we to do | otherwise = case filter (`S.member` todo) neighbours' of -- No neighbours available to be done: it's ready! [] -> Build item -- At least one neighbour is available, search for that one (x : _) -> findReady' stack' visited' x where -- Our neighbours neighbours' = S.toList $ neighbours item graph -- The new visited stack/set stack' = item : stack visited' = S.insert item visited