roshask-0.2: src/executable/Analysis.hs
{-# LANGUAGE OverloadedStrings, TupleSections #-}
module Analysis (MsgInfo, liftIO, getTypeInfo, withMsg, getMsg, addMsg,
runAnalysis, isFlat, SerialInfo(..)) where
import Control.Applicative
import Control.Arrow ((&&&))
import Control.Monad.State
import Data.ByteString.Char8 (pack, unpack, ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.Map as M
import Data.Maybe (isJust)
import System.FilePath (takeFileName, dropExtension)
import Ros.Internal.DepFinder (findMessagesInPkg)
import Types hiding (msgName)
import Parse
import ResolutionTypes
-- Synonym for a Msg paired with its SerialInfo. This tuple is cached
-- for message types as they are visited.
type SerialMsg = (SerialInfo, Msg)
-- Front-end to run analyses.
runAnalysis :: MsgInfo a -> IO a
runAnalysis m = evalStateT (cachePackage "rosgraph_msgs" >> m) emptyMsgContext
-- All the .msg files in a package are cached for quick lookup on
-- subsequent type resolutions.
cachePackage :: ByteString -> MsgInfo PkgCache
cachePackage pkgName = do (dir, msgs) <- liftIO $ findMessagesInPkg (unpack pkgName)
let cache = M.fromList (map prepMsg msgs)
pkg = (dir, M.empty, cache)
alterPkgMap (M.insert pkgName pkg)
return pkg
where prepMsg = pack . dropExtension . takeFileName &&& Left
-- Get a package's 'MsgCache' from the package cache.
getPackage :: ByteString -> MsgInfo PkgCache
getPackage pkgName =
do pkgs <- msgDefs <$> get
maybe (cachePackage pkgName) return $ M.lookup pkgName pkgs
-- Try to get a 'Msg' from a given package. The first argument is the
-- package name, the second argument is the unqualified message name.
getMsgFromPkg :: ByteString -> ByteString -> MsgInfo (Maybe SerialMsg)
getMsgFromPkg pkgName msgName = getPackage pkgName >>= lookupMsg . msgCache
where lookupMsg :: MsgCache -> MsgInfo (Maybe SerialMsg)
lookupMsg cache = maybe (return Nothing)
(return . Just <=< loadMsg)
(M.lookup msgName cache)
loadMsg :: (Either FilePath SerialMsg) -> MsgInfo SerialMsg
loadMsg (Left fp) = either error addMsg =<< liftIO (parseMsg fp)
loadMsg (Right m) = return m
msgCache (_,_,c) = c
getMsg :: ByteString -> MsgInfo SerialMsg
getMsg msgName = check <$>
if B.null msgType
then getMsgFromPkg "rosgraph_msgs" msgName <||>
getMsgFromPkg "std_msgs" msgName <||>
(flip getMsgFromPkg msgName . homePkg =<< get)
else getMsgFromPkg msgPkg (B.tail msgType)
where (msgPkg, msgType) = B.span (/= '/') msgName
check :: Maybe SerialMsg -> SerialMsg
check Nothing = error $ "Couldn't resolve type " ++ unpack msgName
check (Just m) = m
-- checkLocal :: Maybe SerialMsg -> MsgInfo (Maybe SerialMsg)
-- checkLocal Nothing = do home <- homePkg <$> get
-- getMsgFromPkg home msgName
-- checkLocal info = return info
(<||>) :: (Applicative f, Alternative g) => f (g a) -> f (g a) -> f (g a)
(<||>) = liftA2 (<|>)
isFlat :: Msg -> MsgInfo Bool
isFlat = fmap (all isStorable) . mapM (getTypeInfo . fieldType) . fields
isStorable :: SerialInfo -> Bool
isStorable = isJust . size
-- Add bindings for every MsgType referenced by this Msg to a Haskell
-- type and serialization information for that type to a 'MsgInfo'
-- context.
addMsg :: Msg -> MsgInfo SerialMsg
addMsg msg = do oldHome <- homePkg <$> get
let pkgName = B.pack $ msgPackage msg
sName = pack $ shortName msg
tName = B.concat [sName, ".", sName]
setHomePkg pkgName
flat <- isFlat msg
let ser = (if flat then defaultFlat else defaultNonFlat) $ tName
addParsedMsg pkgName sName ser msg
when (not (B.null oldHome)) (setHomePkg oldHome)
return (ser,msg)
withMsg :: Msg -> MsgInfo a -> MsgInfo a
withMsg msg action = do _ <- addMsg msg
oldHome <- homePkg <$> get
setHomePkg . B.pack $ msgPackage msg
r <- action
setHomePkg oldHome
return r
-- Get a 'SerialInfo' value for a given 'MsgType'. If the specified
-- 'MsgType' has not yet been parsed, it will be resolved and parsed.
getTypeInfo :: MsgType -> MsgInfo SerialInfo
getTypeInfo mt = do pkg <- typeCache <$> (getPackage . homePkg =<< get)
aux . M.lookup mt $ pkg
where aux Nothing = addField mt
aux (Just info) = return info
typeCache (_,tc,_) = tc
-- Default SerialInfo value for inductive (non-flat) values
defaultNonFlat :: ByteString -> SerialInfo
defaultNonFlat t = SerialInfo t "put" "get" Nothing
-- Default SerialInfo for a flat value.
defaultFlat :: ByteString -> SerialInfo
defaultFlat t = SerialInfo t "put" "get" (Just si)
where si = B.concat ["sizeOf (P.undefined::", t, ")"]
-- Build a RHS for a sizeOf definition that multiplies a Storable
-- element's size by the number of elements a FixedArray of that type
-- contains.
mulSize :: Int -> SerialInfo -> ByteString
mulSize n (SerialInfo _ _ _ (Just sz)) =
B.concat [pack $ show n, " * (", sz, ")"]
mulSize _ (SerialInfo _ _ _ Nothing) =
error "Can't generate a Storable instance for a RFixedArray\
\with a non-Storable element type."
-- NOTE: ROS specifies that we serialize booleans as a single byte,
-- while there is an instance of Haskell's 'Storable' class for 'Bool'
-- that uses four bytes for each boolean value. We handle individual
-- boolean values with the 'RosBinary' instance for the Haskell 'Bool'
-- type (i.e. one byte for each boolean). We must handle arrays of
-- booleans specially as the serialized form must still be one byte
-- per value.
-- | Deserialization source code string to read a vector of bytes,
-- then convert that to a vector of 'Bool's.
getBoolFromWord :: ByteString
getBoolFromWord = "P.fmap (V.map (P.> 0) :: V.Vector Word.Word8 \
\-> V.Vector P.Bool) get"
-- | Serialization source code string to convert a vector of 'Bool's
-- to a vector of bytes before serializing.
putWordFromBool :: ByteString
putWordFromBool = "(put . (V.map (P.fromIntegral . P.fromEnum)\
\ :: V.Vector P.Bool -> V.Vector Word.Word8))"
-- Add a SerialInfo value for a 'MsgType' to the 'MsgInfo' context.
addField :: MsgType -> MsgInfo SerialInfo
addField RString = ros2Hask RString >>= setTypeInfo RString . defaultNonFlat
addField t@(RVarArray RBool) =
do lst <- vecOf RBool
let arr = SerialInfo lst putWordFromBool getBoolFromWord Nothing
setTypeInfo t arr
addField t@(RVarArray el) =
do elInfo <- getTypeInfo el
arr <- if isStorable elInfo
then do lst <- vecOf el
return $ SerialInfo lst "put" "get" Nothing
else do lst <- listOf el
return $ SerialInfo lst "putList" "getList" Nothing
setTypeInfo t arr
addField t@(RFixedArray n RBool) =
do lst <- vecOf RBool
let arr = SerialInfo lst putWordFromBool getBoolFromWord
(Just . B.pack $ show n)
setTypeInfo t arr
addField t@(RFixedArray n el) =
do elInfo <- getTypeInfo el
arr <- if isStorable elInfo
then do lst <- vecOf el
return $ SerialInfo lst "put" "get"
(Just $ mulSize n elInfo)
else do lst <- listOf el
return $ SerialInfo lst "putFixedList" "getFixedList"
Nothing
setTypeInfo t arr
addField t@(RUserType n) = do userFlat <- isStorable . fst <$> getMsg n
t' <- ros2Hask t
setTypeInfo t $ (if userFlat
then defaultFlat
else defaultNonFlat) t'
addField t = ros2Hask t >>= setTypeInfo t . defaultFlat
-- Generate the name of the Haskell type that corresponds to a flat
-- (i.e. non-array) ROS type.
mkFlatType :: MsgType -> ByteString
mkFlatType RBool = "P.Bool"
mkFlatType RInt8 = "Int.Int8"
mkFlatType RUInt8 = "Word.Word8"
mkFlatType RByte = "Word.Word8"
mkFlatType RChar = "Int.Int8"
mkFlatType RInt16 = "Int.Int16"
mkFlatType RUInt16 = "Word.Word16"
mkFlatType RInt32 = "P.Int"
mkFlatType RUInt32 = "Word.Word32"
mkFlatType RInt64 = "Int.Int64"
mkFlatType RUInt64 = "Word.Word64"
mkFlatType RFloat32 = "P.Float"
mkFlatType RFloat64 = "P.Double"
mkFlatType RTime = "ROSTime"
mkFlatType RDuration = "ROSDuration"
mkFlatType (RUserType t) = qualify . pack . takeFileName . unpack $ t
where qualify b = B.concat [b, ".", b]
mkFlatType t = error $ show t ++ " is not a flat type"
-- Given a home package name and a ROS 'MsgType', generate a Haskell
-- type name.
ros2Hask :: MsgType -> MsgInfo ByteString
ros2Hask (RFixedArray _ t) = mkArrayType t
ros2Hask (RVarArray t) = mkArrayType t
ros2Hask RString = return "P.String"
ros2Hask t = return $ mkFlatType t
vecOf :: MsgType -> MsgInfo ByteString
vecOf = getTypeInfo >=> return . B.append "V.Vector " . hType
listOf :: MsgType -> MsgInfo ByteString
listOf = getTypeInfo >=> return . buildString . hType
where buildString t = B.concat ["[", t, "]"]
-- Make an array type declaration. If the element type of the
-- collection is flat (i.e. does not itself have a field of type
-- 'RVarArray'), then it will have a 'Storable' instance and can be
-- stored in a 'Vector'.
mkArrayType :: MsgType -> MsgInfo ByteString
mkArrayType t = toArr . isStorable =<< getTypeInfo t
where toArr True = vecOf t
toArr False = listOf t