summaryrefslogtreecommitdiff
path: root/src/Hakyll/Core/DependencyAnalyzer.hs
blob: c39b399c4bab662274bb370df33a692d195a71be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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