futhark-0.8.1: src/Futhark/Test/Values.hs
{-# LANGUAGE OverloadedStrings #-}
-- | This module defines an efficient value representation as well as
-- parsing and comparison functions. This is because the standard
-- Futhark parser is not able to cope with large values (like arrays
-- that are tens of megabytes in size). The representation defined
-- here does not support tuples, so don't use those as input/output
-- for your test programs.
module Futhark.Test.Values
( Value(..)
-- * Reading Values
, readValues
-- * Types of values
, ValueType(..)
, valueType
-- * Comparing Values
, compareValues
, Mismatch
, explainMismatch
)
where
import Control.Monad
import Control.Monad.ST
import Data.Binary
import Data.Binary.Put
import Data.Binary.Get
import Data.Binary.IEEE754
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.Maybe
import Data.Int (Int8, Int16, Int32, Int64)
import Data.Char (isSpace, ord, chr)
import Data.Vector.Binary
import qualified Data.Vector.Unboxed.Mutable as UMVec
import qualified Data.Vector.Unboxed as UVec
import Data.Vector.Generic (freeze)
import Data.Loc (Pos(..))
import qualified Language.Futhark.Syntax as F
import Language.Futhark.Pretty()
import Futhark.Representation.Primitive (PrimValue)
import Language.Futhark.Parser.Lexer
import qualified Futhark.Util.Pretty as PP
import Futhark.Representation.AST.Attributes.Constants (IsValue(..))
import Futhark.Representation.AST.Pretty ()
import Futhark.Util.Pretty
type STVector s = UMVec.STVector s
type Vector = UVec.Vector
-- | An efficiently represented Futhark value. Use 'pretty' to get a
-- human-readable representation, and the instances of 'Get' and 'Put'
-- to obtain binary representations
data Value = Int8Value (Vector Int) (Vector Int8)
| Int16Value (Vector Int) (Vector Int16)
| Int32Value (Vector Int) (Vector Int32)
| Int64Value (Vector Int) (Vector Int64)
| Word8Value (Vector Int) (Vector Word8)
| Word16Value (Vector Int) (Vector Word16)
| Word32Value (Vector Int) (Vector Word32)
| Word64Value (Vector Int) (Vector Word64)
| Float32Value (Vector Int) (Vector Float)
| Float64Value (Vector Int) (Vector Double)
| BoolValue (Vector Int) (Vector Bool)
deriving Show
binaryFormatVersion :: Word8
binaryFormatVersion = 2
instance Binary Value where
put (Int8Value shape vs) = putBinaryValue " i8" shape vs putInt8
put (Int16Value shape vs) = putBinaryValue " i16" shape vs putInt16le
put (Int32Value shape vs) = putBinaryValue " i32" shape vs putInt32le
put (Int64Value shape vs) = putBinaryValue " i64" shape vs putInt64le
put (Word8Value shape vs) = putBinaryValue " u8" shape vs putWord8
put (Word16Value shape vs) = putBinaryValue " u16" shape vs putWord16le
put (Word32Value shape vs) = putBinaryValue " u32" shape vs putWord32le
put (Word64Value shape vs) = putBinaryValue " u64" shape vs putWord64le
put (Float32Value shape vs) = putBinaryValue " f32" shape vs putFloat32le
put (Float64Value shape vs) = putBinaryValue " f64" shape vs putFloat64le
put (BoolValue shape vs) = putBinaryValue "bool" shape vs $ putInt8 . boolToInt
where boolToInt True = 1
boolToInt False = 0
get = do
first <- getInt8
version <- getWord8
rank <- getInt8
unless (chr (fromIntegral first) == 'b') $
fail "Input does not begin with ASCII 'b'."
unless (version == binaryFormatVersion) $
fail $ "Expecting binary format version 1; found version: " ++ show version
unless (rank >= 0) $
fail $ "Rank must be non-negative, but is: " ++ show rank
type_f <- getLazyByteString 4
shape <- replicateM (fromIntegral rank) $ fromIntegral <$> getInt64le
let num_elems = product shape
shape' = UVec.fromList shape
case BS.unpack type_f of
" i8" -> get' (Int8Value shape') getInt8 num_elems
" i16" -> get' (Int16Value shape') getInt16le num_elems
" i32" -> get' (Int32Value shape') getInt32le num_elems
" i64" -> get' (Int64Value shape') getInt64le num_elems
" u8" -> get' (Word8Value shape') getWord8 num_elems
" u16" -> get' (Word16Value shape') getWord16le num_elems
" u32" -> get' (Word32Value shape') getWord32le num_elems
" u64" -> get' (Word64Value shape') getWord64le num_elems
" f32" -> get' (Float32Value shape') getFloat32le num_elems
" f64" -> get' (Float64Value shape') getFloat64le num_elems
"bool" -> get' (BoolValue shape') getBool num_elems
s -> fail $ "Cannot parse binary values of type " ++ show s
where getBool = (/=0) <$> getWord8
get' mk get_elem num_elems =
mk <$> genericGetVectorWith (pure num_elems) get_elem
putBinaryValue :: UVec.Unbox a =>
String -> Vector Int -> Vector a -> (a -> Put) -> Put
putBinaryValue tstr shape vs putv = do
putInt8 $ fromIntegral $ ord 'b'
putWord8 binaryFormatVersion
putWord8 $ fromIntegral $ UVec.length shape
mapM_ (putInt8 . fromIntegral . ord) tstr
mapM_ (putInt64le . fromIntegral) $ UVec.toList shape
mapM_ putv $ UVec.toList vs
instance PP.Pretty Value where
ppr v | product (valueShape v) == 0 =
text "empty" <>
parens (dims <> ppr (valueElemType v))
where dims = mconcat $ replicate (length (valueShape v)-1) $ text "[]"
ppr (Int8Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Int16Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Int32Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Int64Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Word8Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Word16Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Word32Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Word64Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Float32Value shape vs) = pprArray (UVec.toList shape) vs
ppr (Float64Value shape vs) = pprArray (UVec.toList shape) vs
ppr (BoolValue shape vs) = pprArray (UVec.toList shape) vs
pprArray :: (UVec.Unbox a, F.IsPrimValue a) => [Int] -> UVec.Vector a -> Doc
pprArray [] vs =
ppr $ F.primValue $ UVec.head vs
pprArray (d:ds) vs =
brackets $ commasep $ map (pprArray ds . slice) [0..d-1]
where slice_size = product ds
slice i = UVec.slice (i*slice_size) slice_size vs
-- | A representation of the simple values we represent in this module.
data ValueType = ValueType [Int] F.PrimType
deriving (Show)
instance PP.Pretty ValueType where
ppr (ValueType ds t) = mconcat (map pprDim ds) <> ppr t
where pprDim d = brackets $ ppr d
-- | A textual description of the type of a value. Follows Futhark
-- type notation, and contains the exact dimension sizes if an array.
valueType :: Value -> ValueType
valueType v = ValueType (valueShape v) $ valueElemType v
valueElemType :: Value -> F.PrimType
valueElemType Int8Value{} = F.Signed F.Int8
valueElemType Int16Value{} = F.Signed F.Int16
valueElemType Int32Value{} = F.Signed F.Int32
valueElemType Int64Value{} = F.Signed F.Int64
valueElemType Word8Value{} = F.Unsigned F.Int8
valueElemType Word16Value{} = F.Unsigned F.Int16
valueElemType Word32Value{} = F.Unsigned F.Int32
valueElemType Word64Value{} = F.Unsigned F.Int64
valueElemType Float32Value{} = F.FloatType F.Float32
valueElemType Float64Value{} = F.FloatType F.Float64
valueElemType BoolValue{} = F.Bool
valueShape :: Value -> [Int]
valueShape (Int8Value shape _) = UVec.toList shape
valueShape (Int16Value shape _) = UVec.toList shape
valueShape (Int32Value shape _) = UVec.toList shape
valueShape (Int64Value shape _) = UVec.toList shape
valueShape (Word8Value shape _) = UVec.toList shape
valueShape (Word16Value shape _) = UVec.toList shape
valueShape (Word32Value shape _) = UVec.toList shape
valueShape (Word64Value shape _) = UVec.toList shape
valueShape (Float32Value shape _) = UVec.toList shape
valueShape (Float64Value shape _) = UVec.toList shape
valueShape (BoolValue shape _) = UVec.toList shape
-- The parser
dropRestOfLine, dropSpaces :: BS.ByteString -> BS.ByteString
dropRestOfLine = BS.drop 1 . BS.dropWhile (/='\n')
dropSpaces t = case BS.dropWhile isSpace t of
t' | "--" `BS.isPrefixOf` t' -> dropSpaces $ dropRestOfLine t'
| otherwise -> t'
type ReadValue v = BS.ByteString -> Maybe (v, BS.ByteString)
symbol :: Char -> BS.ByteString -> Maybe BS.ByteString
symbol c t
| Just (c', t') <- BS.uncons t, c' == c = Just $ dropSpaces t'
| otherwise = Nothing
lexeme :: BS.ByteString -> BS.ByteString -> Maybe BS.ByteString
lexeme l t
| l `BS.isPrefixOf` t = Just $ dropSpaces $ BS.drop (BS.length l) t
| otherwise = Nothing
-- (Used elements, shape, elements, remaining input)
type State s v = (Int, Vector Int, STVector s v, BS.ByteString)
readArrayElemsST :: UMVec.Unbox v =>
Int -> Int -> ReadValue v -> State s v
-> ST s (Maybe (Int, State s v))
readArrayElemsST j r rv s = do
ms <- readRankedArrayOfST r rv s
case ms of
Just (i, shape, arr, t)
| Just t' <- symbol ',' t ->
readArrayElemsST (j+1) r rv (i, shape, arr, t')
| otherwise -> return $ Just (j, (i, shape, arr, t))
_ ->
return $ Just (0, s)
updateShape :: Int -> Int -> Vector Int -> Maybe (Vector Int)
updateShape d n shape
| old_n < 0 = Just $ shape UVec.// [(r-d, n)]
| old_n == n = Just shape
| otherwise = Nothing
where r = UVec.length shape
old_n = shape UVec.! (r-d)
growIfFilled :: UVec.Unbox v => Int -> STVector s v -> ST s (STVector s v)
growIfFilled i arr =
if i >= capacity
then UMVec.grow arr capacity
else return arr
where capacity = UMVec.length arr
readRankedArrayOfST :: UMVec.Unbox v =>
Int -> ReadValue v -> State s v
-> ST s (Maybe (State s v))
readRankedArrayOfST 0 rv (i, shape, arr, t)
| Just (v, t') <- rv t = do
arr' <- growIfFilled i arr
UMVec.write arr' i v
return $ Just (i+1, shape, arr', t')
readRankedArrayOfST r rv (i, shape, arr, t)
| Just t' <- symbol '[' t = do
ms <- readArrayElemsST 1 (r-1) rv (i, shape, arr, t')
return $ do
(j, s) <- ms
closeArray r j s
readRankedArrayOfST _ _ _ =
return Nothing
closeArray :: Int -> Int -> State s v -> Maybe (State s v)
closeArray r j (i, shape, arr, t) = do
t' <- symbol ']' t
shape' <- updateShape r j shape
return (i, shape', arr, t')
readRankedArrayOf :: UMVec.Unbox v =>
Int -> ReadValue v -> BS.ByteString -> Maybe (Vector Int, Vector v, BS.ByteString)
readRankedArrayOf r rv t = runST $ do
arr <- UMVec.new 1024
ms <- readRankedArrayOfST r rv (0, UVec.replicate r (-1), arr, t)
case ms of
Just (i, shape, arr', t') -> do
arr'' <- freeze (UMVec.slice 0 i arr')
return $ Just (shape, arr'', t')
Nothing ->
return Nothing
-- | A character that can be part of a value. This doesn't work for
-- string and character literals.
constituent :: Char -> Bool
constituent ',' = False
constituent ']' = False
constituent ')' = False
constituent c = not $ isSpace c
readIntegral :: Integral int => (Token -> Maybe int) -> ReadValue int
readIntegral f t = do
v <- case fst <$> scanTokens (Pos "" 1 1 0) a of
Right [L _ NEGATE, L _ (INTLIT x)] -> Just $ negate $ fromIntegral x
Right [L _ (INTLIT x)] -> Just $ fromIntegral x
Right [L _ tok] -> f tok
Right [L _ NEGATE, L _ tok] -> negate <$> f tok
_ -> Nothing
return (v, dropSpaces b)
where (a,b) = BS.span constituent t
readInt8 :: ReadValue Int8
readInt8 = readIntegral f
where f (I8LIT x) = Just x
f _ = Nothing
readInt16 :: ReadValue Int16
readInt16 = readIntegral f
where f (I16LIT x) = Just x
f _ = Nothing
readInt32 :: ReadValue Int32
readInt32 = readIntegral f
where f (I32LIT x) = Just x
f _ = Nothing
readInt64 :: ReadValue Int64
readInt64 = readIntegral f
where f (I64LIT x) = Just x
f _ = Nothing
readWord8 :: ReadValue Word8
readWord8 = readIntegral f
where f (U8LIT x) = Just x
f _ = Nothing
readWord16 :: ReadValue Word16
readWord16 = readIntegral f
where f (U16LIT x) = Just x
f _ = Nothing
readWord32 :: ReadValue Word32
readWord32 = readIntegral f
where f (U32LIT x) = Just x
f _ = Nothing
readWord64 :: ReadValue Word64
readWord64 = readIntegral f
where f (U64LIT x) = Just x
f _ = Nothing
readFloat :: RealFloat float => ([Token] -> Maybe float) -> ReadValue float
readFloat f t = do
v <- case map unLoc . fst <$> scanTokens (Pos "" 1 1 0) a of
Right [NEGATE, FLOATLIT x] -> Just $ negate $ fromDouble x
Right [FLOATLIT x] -> Just $ fromDouble x
Right (NEGATE : toks) -> negate <$> f toks
Right toks -> f toks
_ -> Nothing
return (v, dropSpaces b)
where (a,b) = BS.span constituent t
fromDouble = uncurry encodeFloat . decodeFloat
unLoc (L _ x) = x
readFloat32 :: ReadValue Float
readFloat32 = readFloat lexFloat32
where lexFloat32 [F32LIT x] = Just x
lexFloat32 [ID "f32", PROJ_FIELD "inf"] = Just $ 1/0
lexFloat32 [ID "f32", PROJ_FIELD "nan"] = Just $ 0/0
lexFloat32 _ = Nothing
readFloat64 :: ReadValue Double
readFloat64 = readFloat lexFloat64
where lexFloat64 [F64LIT x] = Just x
lexFloat64 [ID "f64", PROJ_FIELD "inf"] = Just $ 1/0
lexFloat64 [ID "f64", PROJ_FIELD "nan"] = Just $ 0/0
lexFloat64 _ = Nothing
readBool :: ReadValue Bool
readBool t = do v <- case fst <$> scanTokens (Pos "" 1 1 0) a of
Right [L _ TRUE] -> Just True
Right [L _ FALSE] -> Just False
_ -> Nothing
return (v, dropSpaces b)
where (a,b) = BS.span constituent t
readPrimType :: ReadValue String
readPrimType t = do
pt <- case fst <$> scanTokens (Pos "" 1 1 0) a of
Right [L _ (ID s)] -> Just $ F.nameToString s
_ -> Nothing
return (pt, dropSpaces b)
where (a,b) = BS.span constituent t
readEmptyArrayOfRank :: Int -> BS.ByteString -> Maybe (Value, BS.ByteString)
readEmptyArrayOfRank r t
| Just t' <- symbol '[' t,
Just t'' <- symbol ']' t' = readEmptyArrayOfRank (r+1) t''
| otherwise = do
(pt, t') <- readPrimType t
v <- case pt of
"i8" -> Just $ Int8Value (UVec.replicate r 0) UVec.empty
"i16" -> Just $ Int16Value (UVec.replicate r 0) UVec.empty
"i32" -> Just $ Int32Value (UVec.replicate r 0) UVec.empty
"i64" -> Just $ Int64Value (UVec.replicate r 0) UVec.empty
"u8" -> Just $ Word8Value (UVec.replicate r 0) UVec.empty
"u16" -> Just $ Word16Value (UVec.replicate r 0) UVec.empty
"u32" -> Just $ Word32Value (UVec.replicate r 0) UVec.empty
"u64" -> Just $ Word64Value (UVec.replicate r 0) UVec.empty
"f32" -> Just $ Float32Value (UVec.replicate r 0) UVec.empty
"f64" -> Just $ Float64Value (UVec.replicate r 0) UVec.empty
"bool" -> Just $ BoolValue (UVec.replicate r 0) UVec.empty
_ -> Nothing
return (v, t')
readEmptyArray :: BS.ByteString -> Maybe (Value, BS.ByteString)
readEmptyArray t = do
t' <- symbol '(' =<< lexeme "empty" t
(v, t'') <- readEmptyArrayOfRank 1 t'
t''' <- symbol ')' t''
return (v, t''')
readValue :: BS.ByteString -> Maybe (Value, BS.ByteString)
readValue full_t
| Right (t', _, v) <- decodeOrFail full_t =
Just (v, dropSpaces t')
| otherwise = readEmptyArray full_t `mplus` insideBrackets 0 full_t
where insideBrackets r t = maybe (tryValueAndReadValue r t) (insideBrackets (r+1)) $ symbol '[' t
tryWith f mk r t
| Just _ <- f t = do
(shape, arr, rest_t) <- readRankedArrayOf r f full_t
return (mk shape arr, rest_t)
| otherwise = Nothing
tryValueAndReadValue r t =
-- 32-bit signed integers come first such that we parse
-- unsuffixed integer constants as of that type.
tryWith readInt32 Int32Value r t `mplus`
tryWith readInt8 Int8Value r t `mplus`
tryWith readInt16 Int16Value r t `mplus`
tryWith readInt64 Int64Value r t `mplus`
tryWith readWord8 Word8Value r t `mplus`
tryWith readWord16 Word16Value r t `mplus`
tryWith readWord32 Word32Value r t `mplus`
tryWith readWord64 Word64Value r t `mplus`
tryWith readFloat64 Float64Value r t `mplus`
tryWith readFloat32 Float32Value r t `mplus`
tryWith readBool BoolValue r t
-- | Parse Futhark values from the given bytestring.
readValues :: BS.ByteString -> Maybe [Value]
readValues = readValues' . dropSpaces
where readValues' t
| BS.null t = Just []
| otherwise = do (a, t') <- readValue t
(a:) <$> readValues' t'
-- Comparisons
-- | Two values differ in some way.
data Mismatch = PrimValueMismatch (Int,Int) PrimValue PrimValue
-- ^ The position the value number and a flat index
-- into the array.
| ArrayShapeMismatch Int [Int] [Int]
| TypeMismatch Int String String
| ValueCountMismatch Int Int
instance Show Mismatch where
show (PrimValueMismatch (i,j) got expected) =
explainMismatch (i,j) "" got expected
show (ArrayShapeMismatch i got expected) =
explainMismatch i "array of shape " got expected
show (TypeMismatch i got expected) =
explainMismatch i "value of type " got expected
show (ValueCountMismatch got expected) =
"Expected " ++ show expected ++ " values, got " ++ show got
-- | A human-readable description of how two values are not the same.
explainMismatch :: (Show i, PP.Pretty a) => i -> String -> a -> a -> String
explainMismatch i what got expected =
"Value " ++ show i ++ " expected " ++ what ++ PP.pretty expected ++ ", got " ++ PP.pretty got
-- | Compare two sets of Futhark values for equality. Shapes and
-- types must also match.
compareValues :: [Value] -> [Value] -> Maybe [Mismatch]
compareValues got expected
| n /= m = Just [ValueCountMismatch n m]
| otherwise = case catMaybes $ zipWith3 compareValue [0..] got expected of
[] -> Nothing
es -> Just es
where n = length got
m = length expected
compareValue :: Int -> Value -> Value -> Maybe Mismatch
compareValue i got_v expected_v
| valueShape got_v == valueShape expected_v =
case (got_v, expected_v) of
(Int8Value _ got_vs, Int8Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Int16Value _ got_vs, Int16Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Int32Value _ got_vs, Int32Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Int64Value _ got_vs, Int64Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Word8Value _ got_vs, Word8Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Word16Value _ got_vs, Word16Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Word32Value _ got_vs, Word32Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Word64Value _ got_vs, Word64Value _ expected_vs) ->
compareNum 1 got_vs expected_vs
(Float32Value _ got_vs, Float32Value _ expected_vs) ->
compareFloat (tolerance expected_vs) got_vs expected_vs
(Float64Value _ got_vs, Float64Value _ expected_vs) ->
compareFloat (tolerance expected_vs) got_vs expected_vs
(BoolValue _ got_vs, BoolValue _ expected_vs) ->
compareGen compareBool got_vs expected_vs
_ ->
Just $ TypeMismatch i (pretty $ valueElemType got_v) (pretty $ valueElemType expected_v)
| otherwise =
Just $ ArrayShapeMismatch i (valueShape got_v) (valueShape expected_v)
where compareNum tol = compareGen $ compareElement tol
compareFloat tol = compareGen $ compareFloatElement tol
compareGen cmp got expected =
foldl mplus Nothing $
zipWith cmp (UVec.toList $ UVec.indexed got) (UVec.toList expected)
compareElement tol (j, got) expected
| comparePrimValue tol got expected = Nothing
| otherwise = Just $ PrimValueMismatch (i,j) (value got) (value expected)
compareFloatElement tol (j, got) expected
| isNaN got, isNaN expected = Nothing
| isInfinite got, isInfinite expected,
signum got == signum expected = Nothing
| otherwise = compareElement tol (j, got) expected
compareBool (j, got) expected
| got == expected = Nothing
| otherwise = Just $ PrimValueMismatch (i,j) (value got) (value expected)
comparePrimValue :: (Ord num, Num num) =>
num -> num -> num -> Bool
comparePrimValue tol x y =
diff < tol
where diff = abs $ x - y
minTolerance :: Fractional a => a
minTolerance = 0.002 -- 0.2%
tolerance :: (Ord a, Fractional a, UVec.Unbox a) => Vector a -> a
tolerance = UVec.foldl tolerance' minTolerance
where tolerance' t v = max t $ minTolerance * v