{-# 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