packages feed

futhark-data-1.0.0.0: src/Futhark/Data.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Strict #-}

-- | This module defines an efficient value representation of the
-- Futhark data format.
module Futhark.Data
  ( Value (..),
    Vector,
    valueText,

    -- * Types of values
    PrimType (..),
    primTypeText,
    primTypeBytes,
    ValueType (..),
    valueTypeTextNoDims,
    valueType,
    valueElemType,
    valueShape,
    valueTypeText,

    -- * Converting values
    GetValue (..),
    PutValue (..),
    valueElems,
  )
where

import Control.Monad
import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Char (chr, ord)
import Data.Int (Int16, Int32, Int64, Int8)
import Data.List (intersperse)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Vector.Storable as SVec
import Data.Vector.Storable.ByteString (byteStringToVector, vectorToByteString)

-- | The value vector type.
type Vector = SVec.Vector

-- | An efficiently represented Futhark value, represented as a shape
-- vector and a value vector, which contains elements in row-major
-- order.  The size of the value vector must be equal to the product
-- of the shape vector.  This is not enforced by the representation,
-- but consuming functions may give unexpected results if this
-- invariant is broken.  Scalars are represented with an empty shape
-- vector.
--
-- Use 'valueText' to get a human-readable representation, and v'put'
-- to obtain binary a representation.
--
-- The 'Eq' instance is the naive one, meaning that no values
-- containing NaNs will be considered equal.  Use the functions from
-- "Futhark.Data.Compare" if this is not what you want.
data Value
  = I8Value (Vector Int) (Vector Int8)
  | I16Value (Vector Int) (Vector Int16)
  | I32Value (Vector Int) (Vector Int32)
  | I64Value (Vector Int) (Vector Int64)
  | U8Value (Vector Int) (Vector Word8)
  | U16Value (Vector Int) (Vector Word16)
  | U32Value (Vector Int) (Vector Word32)
  | U64Value (Vector Int) (Vector Word64)
  | F32Value (Vector Int) (Vector Float)
  | F64Value (Vector Int) (Vector Double)
  | BoolValue (Vector Int) (Vector Bool)
  deriving (Eq, Show)

binaryFormatVersion :: Word8
binaryFormatVersion = 2

instance Binary Value where
  put (I8Value shape vs) = putBinaryValue "  i8" shape vs
  put (I16Value shape vs) = putBinaryValue " i16" shape vs
  put (I32Value shape vs) = putBinaryValue " i32" shape vs
  put (I64Value shape vs) = putBinaryValue " i64" shape vs
  put (U8Value shape vs) = putBinaryValue "  u8" shape vs
  put (U16Value shape vs) = putBinaryValue " u16" shape vs
  put (U32Value shape vs) = putBinaryValue " u32" shape vs
  put (U64Value shape vs) = putBinaryValue " u64" shape vs
  put (F32Value shape vs) = putBinaryValue " f32" shape vs
  put (F64Value shape vs) = putBinaryValue " f64" shape vs
  -- Bool must be treated specially because the Storable instance
  -- uses four bytes.
  put (BoolValue shape vs) = putBinaryValue "bool" shape $ SVec.map boolToInt8 vs
    where
      boolToInt8 True = 1 :: Int8
      boolToInt8 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' = SVec.fromList shape

    case LBS.unpack type_f of
      "  i8" -> get' (I8Value shape') num_elems 1
      " i16" -> get' (I16Value shape') num_elems 2
      " i32" -> get' (I32Value shape') num_elems 4
      " i64" -> get' (I64Value shape') num_elems 8
      "  u8" -> get' (U8Value shape') num_elems 1
      " u16" -> get' (U16Value shape') num_elems 2
      " u32" -> get' (U32Value shape') num_elems 4
      " u64" -> get' (U64Value shape') num_elems 8
      " f32" -> get' (F32Value shape') num_elems 4
      " f64" -> get' (F64Value shape') num_elems 8
      -- Bool must be treated specially because the Storable instance
      -- uses four bytes.
      "bool" -> BoolValue shape' . SVec.map int8ToBool . byteStringToVector . BS.copy <$> getByteString num_elems
      s -> fail $ "Cannot parse binary values of type " ++ show s
    where
      -- The copy is to ensure that the bytestring is properly
      -- aligned.
      get' mk num_elems elem_size =
        mk . byteStringToVector . BS.copy <$> getByteString (num_elems * elem_size)

      int8ToBool :: Int8 -> Bool
      int8ToBool = (/= 0)

putBinaryValue ::
  SVec.Storable a =>
  String ->
  Vector Int ->
  Vector a ->
  Put
putBinaryValue tstr shape vs = do
  putInt8 $ fromIntegral $ ord 'b'
  putWord8 binaryFormatVersion
  putWord8 $ fromIntegral $ SVec.length shape
  mapM_ (putInt8 . fromIntegral . ord) tstr
  putByteString $ vectorToByteString shape
  putByteString $ vectorToByteString vs

arrayText :: (SVec.Storable a) => (a -> TB.Builder) -> [Int] -> SVec.Vector a -> TB.Builder
arrayText p [] vs =
  p $ SVec.head vs
arrayText p (d : ds) vs =
  "[" <> mconcat (intersperse separator $ map (arrayText p ds . slice) [0 .. d -1]) <> "]"
  where
    slice_size = product ds
    slice i = SVec.slice (i * slice_size) slice_size vs
    separator
      | null ds = ", "
      | otherwise = ",\n"

-- | Construct a textual representation of the value as a strict text.
valueText :: Value -> T.Text
valueText v
  | product (valueShape v) == 0 =
    "empty(" <> dims <> primTypeText (valueElemType v) <> ")"
  where
    dims = mconcat $ map (brackets . T.pack . show) $ valueShape v
    brackets s = "[" <> s <> "]"
valueText v =
  case v of
    I8Value shape vs -> f pNum shape vs
    I16Value shape vs -> f pNum shape vs
    I32Value shape vs -> f pNum shape vs
    I64Value shape vs -> f pNum shape vs
    U8Value shape vs -> f pNum shape vs
    U16Value shape vs -> f pNum shape vs
    U32Value shape vs -> f pNum shape vs
    U64Value shape vs -> f pNum shape vs
    F32Value shape vs -> f pF32 shape vs
    F64Value shape vs -> f pF64 shape vs
    BoolValue shape vs -> f pBool shape vs
  where
    suffix = primTypeText $ valueElemType v
    pNum x = TB.fromString (show x) <> TB.fromText suffix
    pF32 x
      | isInfinite x, x >= 0 = "f32.inf"
      | isInfinite x, x < 0 = "-f32.inf"
      | isNaN x = "f32.nan"
      | otherwise = pNum x
    pF64 x
      | isInfinite x, x >= 0 = "f64.inf"
      | isInfinite x, x < 0 = "-f64.inf"
      | isNaN x = "f64.nan"
      | otherwise = pNum x

    pBool True = "true"
    pBool False = "false"

    f p shape vs = LT.toStrict $ TB.toLazyText $ arrayText p (SVec.toList shape) vs

-- | The scalar types supported by the value format.
data PrimType = I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64 | F32 | F64 | Bool
  deriving (Eq, Ord, Show, Enum, Bounded)

-- | Textual primitive type as a strict text.
primTypeText :: PrimType -> T.Text
primTypeText I8 = "i8"
primTypeText I16 = "i16"
primTypeText I32 = "i32"
primTypeText I64 = "i64"
primTypeText U8 = "u8"
primTypeText U16 = "u16"
primTypeText U32 = "u32"
primTypeText U64 = "u64"
primTypeText F32 = "f32"
primTypeText F64 = "f64"
primTypeText Bool = "bool"

-- | The number of bytes taken up by a single element of this type.
primTypeBytes :: PrimType -> Int
primTypeBytes I8 = 1
primTypeBytes I16 = 2
primTypeBytes I32 = 4
primTypeBytes I64 = 8
primTypeBytes U8 = 1
primTypeBytes U16 = 2
primTypeBytes U32 = 4
primTypeBytes U64 = 8
primTypeBytes F32 = 4
primTypeBytes F64 = 8
primTypeBytes Bool = 1

-- | The type of a simple Futhark value, comprising a shape and an
-- element type.
data ValueType = ValueType [Int] PrimType
  deriving (Eq, Ord, Show)

-- | Prettyprint a value type as a strict text.
valueTypeText :: ValueType -> T.Text
valueTypeText (ValueType ds t) = mconcat (map pprDim ds) <> primTypeText t
  where
    pprDim d = "[" <> T.pack (show d) <> "]"

-- | Prettyprint a value type with empty dimensions as a strict text.
-- This is needed for Futhark server programs, whose types are
-- un-sized.
valueTypeTextNoDims :: ValueType -> T.Text
valueTypeTextNoDims (ValueType dims t) =
  mconcat (replicate (length dims) "[]") <> primTypeText t

-- | Get the type of a value.
valueType :: Value -> ValueType
valueType v = ValueType (valueShape v) $ valueElemType v

-- | Get the element type of a value.
valueElemType :: Value -> PrimType
valueElemType I8Value {} = I8
valueElemType I16Value {} = I16
valueElemType I32Value {} = I32
valueElemType I64Value {} = I64
valueElemType U8Value {} = U8
valueElemType U16Value {} = U16
valueElemType U32Value {} = U32
valueElemType U64Value {} = U64
valueElemType F32Value {} = F32
valueElemType F64Value {} = F64
valueElemType BoolValue {} = Bool

-- | The shape of a value.  Empty list in case of a scalar.
valueShape :: Value -> [Int]
valueShape (I8Value shape _) = SVec.toList shape
valueShape (I16Value shape _) = SVec.toList shape
valueShape (I32Value shape _) = SVec.toList shape
valueShape (I64Value shape _) = SVec.toList shape
valueShape (U8Value shape _) = SVec.toList shape
valueShape (U16Value shape _) = SVec.toList shape
valueShape (U32Value shape _) = SVec.toList shape
valueShape (U64Value shape _) = SVec.toList shape
valueShape (F32Value shape _) = SVec.toList shape
valueShape (F64Value shape _) = SVec.toList shape
valueShape (BoolValue shape _) = SVec.toList shape

-- Conversions

-- | Produce a list of the immediate elements of the value.  That is,
-- a 2D array will produce a list of 1D values.  A zero-dimensional
-- value will produce an empty list.  While lists are of course
-- inefficient, the actual values are just slices of the original
-- value, which makes them fairly space-efficient (but beware space
-- leaks).
valueElems :: Value -> [Value]
valueElems v
  | n : ns <- valueShape v =
    let k = product ns
        slices mk vs =
          [ mk (SVec.fromList ns) $
              SVec.slice (k * i) k vs
            | i <- [0 .. n -1]
          ]
     in case v of
          I8Value _ vs -> slices I8Value vs
          I16Value _ vs -> slices I16Value vs
          I32Value _ vs -> slices I32Value vs
          I64Value _ vs -> slices I64Value vs
          U8Value _ vs -> slices U8Value vs
          U16Value _ vs -> slices U16Value vs
          U32Value _ vs -> slices U32Value vs
          U64Value _ vs -> slices U64Value vs
          F32Value _ vs -> slices F32Value vs
          F64Value _ vs -> slices F64Value vs
          BoolValue _ vs -> slices BoolValue vs
  | otherwise =
    []

-- | A class for Haskell values that can be retrieved from 'Value'.
-- This is a convenience facility - don't expect it to be fast.
class GetValue t where
  getValue :: Value -> Maybe t

instance GetValue t => GetValue [t] where
  getValue = mapM getValue . valueElems

instance GetValue Bool where
  getValue (BoolValue shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Int8 where
  getValue (I8Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Int16 where
  getValue (I16Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Int32 where
  getValue (I32Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Int64 where
  getValue (I64Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Word8 where
  getValue (U8Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Word16 where
  getValue (U16Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Word32 where
  getValue (U32Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

instance GetValue Word64 where
  getValue (U64Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

-- | A class for Haskell values that can be converted to 'Value'.
-- This is a convenience facility - don't expect it to be fast.
class PutValue t where
  -- | This may fail for cases such as irregular arrays.
  putValue :: t -> Maybe Value

instance PutValue Int8 where
  putValue = Just . I8Value mempty . SVec.singleton

instance PutValue Int16 where
  putValue = Just . I16Value mempty . SVec.singleton

instance PutValue Int32 where
  putValue = Just . I32Value mempty . SVec.singleton

instance PutValue Int64 where
  putValue = Just . I64Value mempty . SVec.singleton

instance PutValue Word8 where
  putValue = Just . U8Value mempty . SVec.singleton

instance PutValue Word16 where
  putValue = Just . U16Value mempty . SVec.singleton

instance PutValue Word32 where
  putValue = Just . U32Value mempty . SVec.singleton

instance PutValue Word64 where
  putValue = Just . U64Value mempty . SVec.singleton

instance PutValue [Value] where
  putValue [] = Nothing
  putValue (x : xs) = do
    let res_shape = SVec.fromList $ length (x : xs) : valueShape x
    guard $ all ((== valueType x) . valueType) xs
    Just $ case x of
      I8Value {} -> I8Value res_shape $ foldMap getVec (x : xs)
      I16Value {} -> I16Value res_shape $ foldMap getVec (x : xs)
      I32Value {} -> I32Value res_shape $ foldMap getVec (x : xs)
      I64Value {} -> I64Value res_shape $ foldMap getVec (x : xs)
      U8Value {} -> U8Value res_shape $ foldMap getVec (x : xs)
      U16Value {} -> U16Value res_shape $ foldMap getVec (x : xs)
      U32Value {} -> U32Value res_shape $ foldMap getVec (x : xs)
      U64Value {} -> U64Value res_shape $ foldMap getVec (x : xs)
      F32Value {} -> F32Value res_shape $ foldMap getVec (x : xs)
      F64Value {} -> F64Value res_shape $ foldMap getVec (x : xs)
      BoolValue {} -> BoolValue res_shape $ foldMap getVec (x : xs)
    where
      getVec (I8Value _ vec) = SVec.unsafeCast vec
      getVec (I16Value _ vec) = SVec.unsafeCast vec
      getVec (I32Value _ vec) = SVec.unsafeCast vec
      getVec (I64Value _ vec) = SVec.unsafeCast vec
      getVec (U8Value _ vec) = SVec.unsafeCast vec
      getVec (U16Value _ vec) = SVec.unsafeCast vec
      getVec (U32Value _ vec) = SVec.unsafeCast vec
      getVec (U64Value _ vec) = SVec.unsafeCast vec
      getVec (F32Value _ vec) = SVec.unsafeCast vec
      getVec (F64Value _ vec) = SVec.unsafeCast vec
      getVec (BoolValue _ vec) = SVec.unsafeCast vec

instance PutValue T.Text where
  putValue = putValue . T.encodeUtf8

instance PutValue BS.ByteString where
  putValue bs =
    Just $ U8Value size $ byteStringToVector bs
    where
      size = SVec.fromList [fromIntegral (BS.length bs)]