packages feed

futhark-0.19.3: src/Futhark/Test/Values.hs

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

-- | 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 (..),
    Compound (..),
    CompoundValue,
    Vector,

    -- * Reading Values
    readValues,

    -- * Types of values
    ValueType (..),
    prettyValueTypeNoDims,
    valueType,
    valueShape,

    -- * Manipulating values
    valueElems,
    mkCompound,

    -- * Comparing Values
    compareValues,
    Mismatch,

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

import Control.Monad
import Control.Monad.ST
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, isSpace, ord)
import Data.Int (Int16, Int32, Int64, Int8)
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Traversable
import Data.Vector.Generic (freeze)
import qualified Data.Vector.Storable as SVec
import Data.Vector.Storable.ByteString (byteStringToVector, vectorToByteString)
import qualified Data.Vector.Unboxed as UVec
import qualified Data.Vector.Unboxed.Mutable as UMVec
import Futhark.IR.Primitive (PrimValue)
import Futhark.IR.Prop.Constants (IsValue (..))
import Futhark.Util.Loc (Pos (..))
import Futhark.Util.Pretty
import qualified Futhark.Util.Pretty as PP
import Language.Futhark.Parser.Lexer
import Language.Futhark.Pretty ()
import qualified Language.Futhark.Syntax as F

type STVector s = UMVec.STVector s

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

-- | An efficiently represented Futhark value.  Use 'pretty' to get a
-- human-readable representation, and v'put' to obtain binary a
-- representation.
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
  put (Int16Value shape vs) = putBinaryValue " i16" shape vs
  put (Int32Value shape vs) = putBinaryValue " i32" shape vs
  put (Int64Value shape vs) = putBinaryValue " i64" shape vs
  put (Word8Value shape vs) = putBinaryValue "  u8" shape vs
  put (Word16Value shape vs) = putBinaryValue " u16" shape vs
  put (Word32Value shape vs) = putBinaryValue " u32" shape vs
  put (Word64Value shape vs) = putBinaryValue " u64" shape vs
  put (Float32Value shape vs) = putBinaryValue " f32" shape vs
  put (Float64Value 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' (Int8Value shape') num_elems 1
      " i16" -> get' (Int16Value shape') num_elems 2
      " i32" -> get' (Int32Value shape') num_elems 4
      " i64" -> get' (Int64Value shape') num_elems 8
      "  u8" -> get' (Word8Value shape') num_elems 1
      " u16" -> get' (Word16Value shape') num_elems 2
      " u32" -> get' (Word32Value shape') num_elems 4
      " u64" -> get' (Word64Value shape') num_elems 8
      " f32" -> get' (Float32Value shape') num_elems 4
      " f64" -> get' (Float64Value 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

instance PP.Pretty Value where
  ppr v
    | product (valueShape v) == 0 =
      text "empty"
        <> parens (dims <> ppr (valueElemType v))
    where
      dims = mconcat $ map (brackets . ppr) $ valueShape v
  ppr (Int8Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Int16Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Int32Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Int64Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Word8Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Word16Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Word32Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Word64Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Float32Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (Float64Value shape vs) = pprArray (SVec.toList shape) vs
  ppr (BoolValue shape vs) = pprArray (SVec.toList shape) vs

pprArray :: (SVec.Storable a, F.IsPrimValue a) => [Int] -> SVec.Vector a -> Doc
pprArray [] vs =
  ppr $ F.primValue $ SVec.head vs
pprArray (d : ds) vs =
  brackets $ cat $ punctuate separator $ map (pprArray ds . slice) [0 .. d -1]
  where
    slice_size = product ds
    slice i = SVec.slice (i * slice_size) slice_size vs
    separator
      | null ds = comma <> space
      | otherwise = comma <> line

-- | The structure of a compound value, parameterised over the actual
-- values.  For most cases you probably want 'CompoundValue'.
data Compound v
  = ValueRecord (M.Map T.Text (Compound v))
  | -- | Must not be single value.
    ValueTuple [Compound v]
  | ValueAtom v
  deriving (Eq, Ord, Show)

instance Functor Compound where
  fmap = fmapDefault

instance Foldable Compound where
  foldMap = foldMapDefault

instance Traversable Compound where
  traverse f (ValueAtom v) = ValueAtom <$> f v
  traverse f (ValueTuple vs) = ValueTuple <$> traverse (traverse f) vs
  traverse f (ValueRecord m) = ValueRecord <$> traverse (traverse f) m

instance Pretty v => Pretty (Compound v) where
  ppr (ValueAtom v) = ppr v
  ppr (ValueTuple vs) = parens $ commasep $ map ppr vs
  ppr (ValueRecord m) = braces $ commasep $ map field $ M.toList m
    where
      field (k, v) = ppr k <> equals <> ppr v

-- | Create a tuple for a non-unit list, and otherwise a 'ValueAtom'
mkCompound :: [v] -> Compound v
mkCompound [v] = ValueAtom v
mkCompound vs = ValueTuple $ map ValueAtom vs

-- | Like a 'Value', but also grouped in compound ways that are not
-- supported by raw values.  You cannot parse or read these in
-- standard ways, and they cannot be elements of arrays.
type CompoundValue = Compound Value

-- | A representation of the simple values we represent in this module.
data ValueType = ValueType [Int] F.PrimType
  deriving (Eq, Ord, Show)

instance PP.Pretty ValueType where
  ppr (ValueType ds t) = mconcat (map pprDim ds) <> ppr t
    where
      pprDim d = brackets $ ppr d

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

-- | Get the type of a value.
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

-- | The shape of a value.  Empty list in case of a scalar.
valueShape :: Value -> [Int]
valueShape (Int8Value shape _) = SVec.toList shape
valueShape (Int16Value shape _) = SVec.toList shape
valueShape (Int32Value shape _) = SVec.toList shape
valueShape (Int64Value shape _) = SVec.toList shape
valueShape (Word8Value shape _) = SVec.toList shape
valueShape (Word16Value shape _) = SVec.toList shape
valueShape (Word32Value shape _) = SVec.toList shape
valueShape (Word64Value shape _) = SVec.toList shape
valueShape (Float32Value shape _) = SVec.toList shape
valueShape (Float64Value shape _) = SVec.toList shape
valueShape (BoolValue shape _) = SVec.toList shape

-- | Produce a list of the immediate elements of the value.  That is,
-- a 2D array will produce a list of 1D values.  While lists are of
-- course inefficient, the actual values are just slices of the
-- original value, which makes them fairly efficient.
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
          Int8Value _ vs -> slices Int8Value vs
          Int16Value _ vs -> slices Int16Value vs
          Int32Value _ vs -> slices Int32Value vs
          Int64Value _ vs -> slices Int64Value vs
          Word8Value _ vs -> slices Word8Value vs
          Word16Value _ vs -> slices Word16Value vs
          Word32Value _ vs -> slices Word32Value vs
          Word64Value _ vs -> slices Word64Value vs
          Float32Value _ vs -> slices Float32Value vs
          Float64Value _ vs -> slices Float64Value vs
          BoolValue _ vs -> slices BoolValue vs
  | otherwise =
    []

-- The parser

dropRestOfLine, dropSpaces :: LBS.ByteString -> LBS.ByteString
dropRestOfLine = LBS.drop 1 . LBS.dropWhile (/= '\n')
dropSpaces t = case LBS.dropWhile isSpace t of
  t'
    | "--" `LBS.isPrefixOf` t' -> dropSpaces $ dropRestOfLine t'
    | otherwise -> t'

type ReadValue v = LBS.ByteString -> Maybe (v, LBS.ByteString)

symbol :: Char -> LBS.ByteString -> Maybe LBS.ByteString
symbol c t
  | Just (c', t') <- LBS.uncons t, c' == c = Just $ dropSpaces t'
  | otherwise = Nothing

lexeme :: LBS.ByteString -> LBS.ByteString -> Maybe LBS.ByteString
lexeme l t
  | l `LBS.isPrefixOf` t = Just $ dropSpaces $ LBS.drop (LBS.length l) t
  | otherwise = Nothing

-- (Used elements, shape, elements, remaining input)
type State s v = (Int, Vector Int, STVector s v, LBS.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 -> do
        next <- readArrayElemsST (j + 1) r rv (i, shape, arr, t')
        -- Not OK to have zero values after a comma.
        case next of
          Just (0, _) -> return Nothing
          _ -> return next
      | 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 SVec.// [(r - d, n)]
  | old_n == n = Just shape
  | otherwise = Nothing
  where
    r = SVec.length shape
    old_n = shape SVec.! (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, SVec.Storable v) =>
  Int ->
  ReadValue v ->
  LBS.ByteString ->
  Maybe (Vector Int, Vector v, LBS.ByteString)
readRankedArrayOf r rv t = runST $ do
  arr <- UMVec.new 1024
  ms <- readRankedArrayOfST r rv (0, SVec.replicate r (-1), arr, t)
  case ms of
    Just (i, shape, arr', t') -> do
      arr'' <- freeze (UMVec.slice 0 i arr')
      return $ Just (shape, UVec.convert 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) = LBS.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) = LBS.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) = LBS.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) = LBS.span constituent t

readEmptyArrayOfShape :: [Int] -> LBS.ByteString -> Maybe (Value, LBS.ByteString)
readEmptyArrayOfShape shape t
  | Just t' <- symbol '[' t,
    Just (d, t'') <- readIntegral (const Nothing) t',
    Just t''' <- symbol ']' t'' =
    readEmptyArrayOfShape (shape ++ [d]) t'''
  | otherwise = do
    (pt, t') <- readPrimType t
    guard $ elem 0 shape
    v <- case pt of
      "i8" -> Just $ Int8Value (SVec.fromList shape) SVec.empty
      "i16" -> Just $ Int16Value (SVec.fromList shape) SVec.empty
      "i32" -> Just $ Int32Value (SVec.fromList shape) SVec.empty
      "i64" -> Just $ Int64Value (SVec.fromList shape) SVec.empty
      "u8" -> Just $ Word8Value (SVec.fromList shape) SVec.empty
      "u16" -> Just $ Word16Value (SVec.fromList shape) SVec.empty
      "u32" -> Just $ Word32Value (SVec.fromList shape) SVec.empty
      "u64" -> Just $ Word64Value (SVec.fromList shape) SVec.empty
      "f32" -> Just $ Float32Value (SVec.fromList shape) SVec.empty
      "f64" -> Just $ Float64Value (SVec.fromList shape) SVec.empty
      "bool" -> Just $ BoolValue (SVec.fromList shape) SVec.empty
      _ -> Nothing
    return (v, t')

readEmptyArray :: LBS.ByteString -> Maybe (Value, LBS.ByteString)
readEmptyArray t = do
  t' <- symbol '(' =<< lexeme "empty" t
  (v, t'') <- readEmptyArrayOfShape [] t'
  t''' <- symbol ')' t''
  return (v, t''')

readValue :: LBS.ByteString -> Maybe (Value, LBS.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 :: LBS.ByteString -> Maybe [Value]
readValues = readValues' . dropSpaces
  where
    readValues' t
      | LBS.null t = Just []
      | otherwise = do
        (a, t') <- readValue t
        (a :) <$> readValues' t'

-- Comparisons

-- | Two values differ in some way.  The 'Show' instance produces a
-- human-readable explanation.
data Mismatch
  = -- | The position the value number and a flat index
    -- into the array.
    PrimValueMismatch (Int, Int) PrimValue PrimValue
  | 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] -> [Mismatch]
compareValues got expected
  | n /= m = [ValueCountMismatch n m]
  | otherwise = concat $ zipWith3 compareValue [0 ..] got expected
  where
    n = length got
    m = length expected

compareValue :: Int -> Value -> Value -> [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
      _ ->
        [TypeMismatch i (pretty $ valueElemType got_v) (pretty $ valueElemType expected_v)]
  | otherwise =
    [ArrayShapeMismatch i (valueShape got_v) (valueShape expected_v)]
  where
    {-# INLINE compareGen #-}
    {-# INLINE compareNum #-}
    {-# INLINE compareFloat #-}
    {-# INLINE compareFloatElement #-}
    {-# INLINE compareElement #-}
    compareNum tol = compareGen $ compareElement tol
    compareFloat tol = compareGen $ compareFloatElement tol

    compareGen cmp got expected =
      let l = SVec.length got
          check acc j
            | j < l =
              case cmp j (got SVec.! j) (expected SVec.! j) of
                Just mismatch ->
                  check (mismatch : acc) (j + 1)
                Nothing ->
                  check acc (j + 1)
            | otherwise =
              acc
       in reverse $ check [] 0

    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 :: (RealFloat a, SVec.Storable a) => Vector a -> a
tolerance = SVec.foldl tolerance' minTolerance . SVec.filter (not . nanOrInf)
  where
    tolerance' t v = max t $ minTolerance * v
    nanOrInf x = isInfinite x || isNaN x

-- | 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 (Int8Value shape vs)
    | [] <- SVec.toList shape =
      Just $ vs SVec.! 0
  getValue _ = Nothing

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

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

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

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

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

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

instance GetValue Word64 where
  getValue (Word64Value 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 Word8 where
  putValue = Just . Word8Value mempty . SVec.singleton

instance PutValue F.PrimValue where
  putValue (F.SignedValue (F.Int8Value x)) =
    Just $ Int8Value mempty $ SVec.singleton x
  putValue (F.SignedValue (F.Int16Value x)) =
    Just $ Int16Value mempty $ SVec.singleton x
  putValue (F.SignedValue (F.Int32Value x)) =
    Just $ Int32Value mempty $ SVec.singleton x
  putValue (F.SignedValue (F.Int64Value x)) =
    Just $ Int64Value mempty $ SVec.singleton x
  putValue (F.UnsignedValue (F.Int8Value x)) =
    Just $ Word8Value mempty $ SVec.singleton $ fromIntegral x
  putValue (F.UnsignedValue (F.Int16Value x)) =
    Just $ Word16Value mempty $ SVec.singleton $ fromIntegral x
  putValue (F.UnsignedValue (F.Int32Value x)) =
    Just $ Word32Value mempty $ SVec.singleton $ fromIntegral x
  putValue (F.UnsignedValue (F.Int64Value x)) =
    Just $ Word64Value mempty $ SVec.singleton $ fromIntegral x
  putValue (F.FloatValue (F.Float32Value x)) =
    Just $ Float32Value mempty $ SVec.singleton x
  putValue (F.FloatValue (F.Float64Value x)) =
    Just $ Float64Value mempty $ SVec.singleton x
  putValue (F.BoolValue b) =
    Just $ BoolValue mempty $ SVec.singleton b

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
      Int8Value {} -> Int8Value res_shape $ foldMap getVec (x : xs)
      Int16Value {} -> Int16Value res_shape $ foldMap getVec (x : xs)
      Int32Value {} -> Int32Value res_shape $ foldMap getVec (x : xs)
      Int64Value {} -> Int64Value res_shape $ foldMap getVec (x : xs)
      Word8Value {} -> Word8Value res_shape $ foldMap getVec (x : xs)
      Word16Value {} -> Word16Value res_shape $ foldMap getVec (x : xs)
      Word32Value {} -> Word32Value res_shape $ foldMap getVec (x : xs)
      Word64Value {} -> Word64Value res_shape $ foldMap getVec (x : xs)
      Float32Value {} -> Float32Value res_shape $ foldMap getVec (x : xs)
      Float64Value {} -> Float64Value res_shape $ foldMap getVec (x : xs)
      BoolValue {} -> BoolValue res_shape $ foldMap getVec (x : xs)
    where
      getVec (Int8Value _ vec) = SVec.unsafeCast vec
      getVec (Int16Value _ vec) = SVec.unsafeCast vec
      getVec (Int32Value _ vec) = SVec.unsafeCast vec
      getVec (Int64Value _ vec) = SVec.unsafeCast vec
      getVec (Word8Value _ vec) = SVec.unsafeCast vec
      getVec (Word16Value _ vec) = SVec.unsafeCast vec
      getVec (Word32Value _ vec) = SVec.unsafeCast vec
      getVec (Word64Value _ vec) = SVec.unsafeCast vec
      getVec (Float32Value _ vec) = SVec.unsafeCast vec
      getVec (Float64Value _ 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 $ Word8Value size $ byteStringToVector bs
    where
      size = SVec.fromList [fromIntegral (BS.length bs)]