rattle-0.2: src/Development/Rattle/Hazards.hs
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RecordWildCards #-}
module Development.Rattle.Hazards(
Hazard(..), Recoverable(..),
HazardSet, mergeHazardSet, newHazardSet, emptyHazardSet, seenHazardSet, addHazardSet,
recoverableHazard, restartableHazard,
) where
import Development.Rattle.Types
import Control.Exception.Extra
import System.Time.Extra
import General.Extra
import Data.List
import Data.Tuple.Extra
import qualified Data.HashMap.Strict as Map
import General.FileName
data ReadOrWrite = Read | Write deriving (Show,Eq)
-- For Write, Seconds is the last possible time at which it was written
-- For Read, Seconds is the earliest possible time at which it was read
-- In both cases, Cmd is the thing that caused the read/write
newtype HazardSet = HazardSet (Map.HashMap FileName (ReadOrWrite, Seconds, Cmd))
deriving Show
-- | Type of exception thrown if there is a hazard when running the build system.
data Hazard
= ReadWriteHazard FileName Cmd Cmd Recoverable
| WriteWriteHazard FileName Cmd Cmd Recoverable
deriving Show
instance Exception Hazard
data Recoverable = Recoverable | NonRecoverable | Restartable deriving (Show,Eq)
recoverableHazard :: Hazard -> Bool
recoverableHazard WriteWriteHazard{} = False
recoverableHazard (ReadWriteHazard _ _ _ r) = r == Recoverable
restartableHazard :: Hazard -> Bool
restartableHazard (WriteWriteHazard _ _ _ r) = r == Restartable
restartableHazard (ReadWriteHazard _ _ _ r) = r == Restartable
emptyHazardSet :: HazardSet
emptyHazardSet = HazardSet Map.empty
seenHazardSet :: FileName -> HazardSet -> Bool
seenHazardSet x (HazardSet mp) = x `Map.member` mp
newHazardSet :: Seconds -> Seconds -> Cmd -> Touch FileName -> HazardSet
newHazardSet start stop cmd Touch{..} = HazardSet $ Map.fromList $
map (,(Write,stop ,cmd)) tWrite ++
map (,(Read ,start,cmd)) tRead
mergeHazardSet :: [Cmd] -> HazardSet -> HazardSet -> ([Hazard], HazardSet)
mergeHazardSet required (HazardSet h1) (HazardSet h2) =
second HazardSet $ unionWithKeyEithers (mergeFileOps required) h1 h2
-- | addHazardSet a b c d e f == mergeHazardSet a b (newHazardSet c d e f)
addHazardSet :: [Cmd] -> HazardSet -> Seconds -> Seconds -> Cmd -> Touch FileName -> ([Hazard], HazardSet)
addHazardSet required (HazardSet h1) start stop cmd Touch{..} =
second HazardSet $ insertWithKeyEithers (mergeFileOps required) h1 $
map (,(Write,stop,cmd)) tWrite ++ map (,(Read,start,cmd)) tRead
-- Very carefully written to include the commands
{- HLINT ignore mergeFileOps "Redundant if" -}
{- HLINT ignore mergeFileOps "Use infix" -}
-- r is required list; s is speculate list
mergeFileOps :: [Cmd] -> FileName -> (ReadOrWrite, Seconds, Cmd) -> (ReadOrWrite, Seconds, Cmd) -> Either Hazard (Maybe (ReadOrWrite, Seconds, Cmd))
mergeFileOps r x (Read, t1, cmd1) (Read, t2, cmd2)
| t1 <= t2 = Right Nothing -- don't update the Map (an optimisation for the common case)
| otherwise = Right $ Just (Read, t2, cmd2)
mergeFileOps r x (Write, t1, cmd1) (Write, t2, cmd2) = Left $ WriteWriteHazard x cmd1 cmd2 $
-- if they both were required, we've got a problem
if elem cmd1 r && elem cmd2 r then NonRecoverable
-- if one (or both) were speculated, we might be able to restart and get over it
else Restartable
mergeFileOps r x (Read, tR, cmdR) (Write, tW, cmdW)
| tW < tR = -- write happened first
-- if the write hasn't been demanded but the read has we've
-- managed to read something that was speculated, which is bad
if elem cmdR r && notElem cmdW r then hazard Restartable
-- otherwise, everything is good
else Right $ Just (Write, tW, cmdW)
| otherwise = -- read happened first
-- if the read was speculated, we can ignore it
if notElem cmdR r then hazard Recoverable
-- if the write was speculated, we can restart and hopefully it won't recur
else if notElem cmdW r then hazard Restartable
-- neither was speculated, but did we use speculation to reorder them?
-- note the order seems backwards, because r is a snoc-list
-- FIXME: We might have had them race because of parallelism, so this is optimistically restartable
else if elemIndex cmdR r < elemIndex cmdW r then hazard Restartable
-- the user wrote the read before the write
else hazard NonRecoverable
where
hazard = Left . ReadWriteHazard x cmdW cmdR
mergeFileOps r x v1 v2 = mergeFileOps r x v2 v1 -- must be Write/Read, so match the other way around