rattle-0.2: src/Development/Rattle/Types.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveTraversable #-}
{-# LANGUAGE StandaloneDeriving, DeriveGeneric #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Development.Rattle.Types(
Trace(..), Touch(..), fsaTrace, normalizeTouch,
TouchSet, tsRead, tsWrite, newTouchSet, addTouchSet,
Cmd(..), mkCmd,
RunIndex, runIndex0, nextRunIndex,
) where
import Data.Hashable
import Data.List.Extra
import System.Directory
import System.Info.Extra
import Control.Monad
import General.Binary
import Data.Word
import Development.Shake.Command
import Data.Semigroup
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString as BS
import qualified Data.HashSet as Set
import GHC.Generics
import Prelude
import System.Time.Extra
import General.FileName
-- record the hash as the first field
data Cmd = Cmd Int [CmdOption] [String]
deriving Eq
instance Show Cmd where
show (Cmd _ a b) = "Cmd " ++ show a ++ " " ++ show b
instance Hashable Cmd where
hashWithSalt _ = hash
hash (Cmd x _ _) = x
mkCmd :: [CmdOption] -> [String] -> Cmd
mkCmd a b = Cmd (hash (a,b)) a b
instance BinaryEx Cmd where
getEx x = mkCmd (getEx a) (getEx b)
where (a,b) = getExPair x
putEx (Cmd _ a b) = putExPair (putEx a) (putEx b)
-- The common values for CmdOption are [], [Shell] and a few others - optimise those
instance BinaryEx [CmdOption] where
getEx x
| BS.null x = []
| BS.length x == 1 = case getEx x :: Word8 of
0 -> [Shell]
1 -> [EchoStderr False]
2 -> [Shell,EchoStderr False]
| otherwise = map read $ getEx x
putEx [] = mempty
putEx [Shell] = putEx (0 :: Word8)
putEx [EchoStderr False] = putEx (1 :: Word8)
putEx [Shell,EchoStderr False] = putEx (2 :: Word8)
putEx xs = putEx $ map show xs
deriving instance Generic CmdOption
deriving instance Read CmdOption
instance Hashable CmdOption
data Trace a = Trace
{tRun :: {-# UNPACK #-} !RunIndex
,tStart :: {-# UNPACK #-} !Seconds
,tStop :: {-# UNPACK #-} !Seconds
,tTouch :: Touch a
} deriving (Show, Functor, Foldable, Traversable, Eq)
instance BinaryEx a => BinaryEx (Trace a) where
getEx x = Trace a b c $ getEx d
where (a,b,c,d) = binarySplit3 x
putEx (Trace a b c d) = putExStorable a <> putExStorable b <> putExStorable c <> putEx d
data Touch a = Touch
{tRead :: [a]
,tWrite :: [a]
} deriving (Show, Functor, Foldable, Traversable, Eq)
instance BinaryEx a => BinaryEx (Touch a) where
getEx x = Touch (map getEx $ getExList a) (map getEx $ getExList b)
where [a,b] = getExList x
putEx (Touch a b) = putExList [putExList $ map putEx a, putExList $ map putEx b]
instance Semigroup (Touch a) where
Touch r1 w1 <> Touch r2 w2 = Touch (r1++r2) (w1++w2)
instance Monoid (Touch a) where
mempty = Touch [] []
mappend = (<>)
mconcat xs = Touch (concatMap tRead xs) (concatMap tWrite xs)
instance Hashable a => Hashable (Trace a) where
hashWithSalt s (Trace a b c d) = hashWithSalt s (a,b,c,d)
instance Hashable a => Hashable (Touch a) where
hashWithSalt s (Touch r w) = hashWithSalt s (r,w)
fsaTrace :: [FSATrace BS.ByteString] -> IO (Touch FileName)
-- We want to get normalized traces. On Linux, things come out normalized, and we just want to dedupe them
-- On Windows things come out as C:\windows\system32\KERNELBASE.dll instead of C:\Windows\System32\KernelBase.dll
-- so important to call (expensive) normalizeTouch
fsaTrace fs
| isWindows =
-- normalize twice because normalisation is cheap, but canonicalisation might be expensive
fmap (normalizeTouch . fmap (byteStringToFileName . UTF8.fromString)) $
canonicalizeTouch $
fmap UTF8.toString $ normalizeTouch $ mconcatMap f fs
| otherwise =
-- We know the file names are already normalized from Shake so avoid a redundant conversion
pure $ normalizeTouch $ byteStringToFileName <$> mconcatMap f fs
where
f (FSAWrite x) = Touch [] [x]
f (FSARead x) = Touch [x] []
f (FSADelete x) = Touch [] [x]
f (FSAMove x y) = Touch [] [x,y]
f (FSAQuery x) = Touch [x] []
f (FSATouch x) = Touch [] [x]
normalizeTouch :: (Ord a, Hashable a) => Touch a -> Touch a
-- added 'sort' because HashSet uses the ordering of the hashes, which is confusing
-- and since we are sorting, try and avoid doing too much hash manipulation of the reads
normalizeTouch (Touch a b) = Touch (f $ sort a) (sort $ Set.toList b2)
where
b2 = Set.fromList b
f (x1:x2:xs) | x1 == x2 = f (x1:xs)
f (x:xs) | x `Set.member` b2 = f xs
| otherwise = x : f xs
f [] = []
canonicalizeTouch :: Touch FilePath -> IO (Touch FilePath)
canonicalizeTouch (Touch a b) = Touch <$> mapM canonicalizePath a <*> mapM canonicalizePath b
-- For sets, Set.fromList is fastest if there are no dupes
-- Otherwise a Set.member/Set.insert is fastest
data TouchSet = TouchSet {tsRead :: Set.HashSet FileName, tsWrite :: Set.HashSet FileName}
newTouchSet :: [Touch FileName] -> TouchSet
newTouchSet [] = TouchSet Set.empty Set.empty
newTouchSet (Touch{..}:xs) = foldl' addTouchSet (TouchSet (Set.fromList tRead) (Set.fromList tWrite)) xs
addTouchSet :: TouchSet -> Touch FileName -> TouchSet
addTouchSet TouchSet{..} Touch{..} = TouchSet (f tsRead tRead) (f tsWrite tWrite)
where f = foldl' (\mp k -> if Set.member k mp then mp else Set.insert k mp)
-- | Which run we are in, monotonically increasing
newtype RunIndex = RunIndex Int
deriving (Eq,Ord,Show,Storable,BinaryEx,Hashable)
runIndex0 :: RunIndex
runIndex0 = RunIndex 0
nextRunIndex :: RunIndex -> RunIndex
nextRunIndex (RunIndex i) = RunIndex $ i + 1