packages feed

wireform-flatbuffers-0.1.0.0: test/Test/FlatBuffers/Builder/Reference.hs

{-# LANGUAGE BangPatterns #-}


{- | The original list-of-chunks "FlatBuffers.Builder", kept verbatim
(minus comments) as a byte-for-byte oracle: the rewritten builder must
emit exactly the same buffers for the same calls.
-}
module Test.FlatBuffers.Builder.Reference (
  Builder,
  newBuilder,
  finish,
  currentUOff,

  prependBS,
  prependU8,
  prependU16,
  prependU32,
  prependU64,
  prependI16,
  prependI32,
  prependI64,
  prepForObject,
  noteMinAlign,

  Field' (..),
  scalar,
  struct,
  voff,
  writeTable,

  writeString,
  writeVectorOfOffsets,
  writeVectorOfStructs,
  writeVectorInt32,
  writeVectorInt64,

  alignUp,
) where

import Data.Bits (complement, (.&.))
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Internal qualified as BSI
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Int (Int16, Int32, Int64)
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Word (Word16, Word32, Word64, Word8)
import Foreign.Storable (pokeByteOff)


data Builder = Builder
  { bufPayload :: !(IORef [BS.ByteString])

  , bufSize :: !(IORef Int)
  , bufVTables :: !(IORef (Map.Map VTableKey Int))

  , bufMinAlign :: !(IORef Int)
  }


newtype VTableKey = VTableKey (Int, Int, [Int])
  deriving stock (Eq, Ord)


newBuilder :: IO Builder
newBuilder = Builder <$> newIORef [] <*> newIORef 0 <*> newIORef Map.empty <*> newIORef 1


noteMinAlign :: Builder -> Int -> IO ()
noteMinAlign b a = modifyIORef' (bufMinAlign b) (max a)


prepForObject :: Builder -> Int -> Int -> IO ()
prepForObject b objSize objAlign = do
  noteMinAlign b objAlign
  !cur <- readIORef (bufSize b)
  let !after = cur + objSize
      !pad = (negate after) .&. (objAlign - 1)
  prependBS b (BS.replicate pad 0)


prependBS :: Builder -> BS.ByteString -> IO ()
prependBS b bs = do
  modifyIORef' (bufPayload b) (bs :)
  modifyIORef' (bufSize b) (+ BS.length bs)


prependU8 :: Builder -> Word8 -> IO ()
prependU8 b w = prependBS b (BS.singleton w)


prependU16 :: Builder -> Word16 -> IO ()
prependU16 b w = prependBS b $ BS.pack [fromIntegral w, fromIntegral (w `div` 0x100)]


prependU32 :: Builder -> Word32 -> IO ()
prependU32 b w =
  prependBS b $
    BS.pack
      [ fromIntegral w
      , fromIntegral (w `div` 0x100)
      , fromIntegral (w `div` 0x10000)
      , fromIntegral (w `div` 0x1000000)
      ]


prependU64 :: Builder -> Word64 -> IO ()
prependU64 b w = do
  prependU32 b (fromIntegral (w `div` 0x100000000))
  prependU32 b (fromIntegral w)


prependI16 :: Builder -> Int16 -> IO ()
prependI16 b i = prependU16 b (fromIntegral i)


prependI32 :: Builder -> Int32 -> IO ()
prependI32 b i = prependU32 b (fromIntegral i)


prependI64 :: Builder -> Int64 -> IO ()
prependI64 b i = prependU64 b (fromIntegral i)


finish :: Builder -> Int -> IO ByteString
finish b rootUOff = do
  !minA <- readIORef (bufMinAlign b)
  prepForObject b 4 (max minA 4)
  !curBefore <- readIORef (bufSize b)
  let !rootFromStart = (curBefore + 4) - rootUOff
  prependU32 b (fromIntegral rootFromStart)
  chunks <- readIORef (bufPayload b)
  pure $! BS.concat chunks


currentUOff :: Builder -> IO Int
currentUOff b = readIORef (bufSize b)


data Field' = Field'
  { fsAlign :: !Int
  , fsWrite :: !(Builder -> Int -> IO ())

  }


scalar :: Int -> (Builder -> IO ()) -> Field'
scalar !align writer = Field' align $ \b _ -> writer b


struct :: Int -> Int -> (Builder -> IO ()) -> Field'
struct !size !_align writer = Field' size $ \b _ -> writer b


voff :: Int -> Field'
voff !targetUOff = Field' 4 $ \b _ -> do
  cur <- currentUOff b
  prependU32 b (fromIntegral (cur + 4 - targetUOff))


writeTable :: Builder -> [Maybe Field'] -> IO Int
writeTable b slots = do
  let !present = collectPresent 0 slots
      !nSlots = length slots
      layout !_pos [] = ([], 0)
      layout !pos ((idx, fs) : rest) =
        let !padPos = alignUp pos (fsAlign fs)
            !pos' = padPos + fsAlign fs
            (rs, end) = layout pos' rest
        in ((idx, padPos) : rs, max pos' end)
      (inlineOffs, rawEnd) = layout 4 present
      !maxAlign = foldr (\(_, fs) m -> max m (fsAlign fs)) 4 present
      !tableSize = alignUp rawEnd maxAlign

  prepForObject b tableSize maxAlign

  prependBS b (BS.replicate (tableSize - rawEnd) 0)

  let emit !nextExpect [] = prependBS b (BS.replicate (nextExpect - 4) 0)
      emit !expectedEnd ((idx, fs) : rest) = do
        let !off = case lookup idx inlineOffs of
              Just o -> o
              Nothing -> error "FlatBuffers.Builder: internal error (missing inlineOff for present slot)"
            !fieldEnd = off + fsAlign fs
            !padAfter = expectedEnd - fieldEnd
        prependBS b (BS.replicate padAfter 0)
        fsWrite fs b 0
        emit off rest
  emit rawEnd (reverse present)

  curBeforeSoff <- currentUOff b
  let !tableUOff = curBeforeSoff + 4
      (vtKey, vtBytesCount, vtBytes) = makeVTableBytes inlineOffs tableSize nSlots
  dedup <- readIORef (bufVTables b)
  case Map.lookup vtKey dedup of
    Just existingUOff -> do
      prependI32 b (fromIntegral (existingUOff - tableUOff))
      pure tableUOff
    Nothing -> do
      prependI32 b (fromIntegral vtBytesCount)
      prependBS b vtBytes
      newU <- currentUOff b
      modifyIORef' (bufVTables b) (Map.insert vtKey newU)
      pure tableUOff


collectPresent :: Int -> [Maybe Field'] -> [(Int, Field')]
collectPresent !_ [] = []
collectPresent !i (Nothing : xs) = collectPresent (i + 1) xs
collectPresent !i (Just fs : xs) = (i, fs) : collectPresent (i + 1) xs


makeVTableBytes
  :: [(Int, Int)]
  -> Int
  -> Int
  -> (VTableKey, Int, BS.ByteString)
makeVTableBytes present tableSize nSlots =
  let slotMap = Map.fromList present
      slots = mkSlots 0 slotMap nSlots
      trimmed = reverse (dropWhile (== 0) (reverse slots))
      !nT = length trimmed
      !vtSize = 2 + 2 + 2 * nT
      !bytes = BSI.unsafeCreate vtSize $ \p -> do
        pokeByteOff p 0 (fromIntegral vtSize :: Word8)
        pokeByteOff p 1 (fromIntegral (vtSize `div` 0x100) :: Word8)
        pokeByteOff p 2 (fromIntegral tableSize :: Word8)
        pokeByteOff p 3 (fromIntegral (tableSize `div` 0x100) :: Word8)
        let writeSlot !i (s : ss) = do
              pokeByteOff p (4 + 2 * i) (fromIntegral s :: Word8)
              pokeByteOff p (4 + 2 * i + 1) (fromIntegral (s `div` 0x100) :: Word8)
              writeSlot (i + 1) ss
            writeSlot !_ [] = pure ()
        writeSlot 0 trimmed
      !key = VTableKey (vtSize, tableSize, trimmed)
  in (key, vtSize, bytes)
  where
    mkSlots !i !_ !n | i >= n = []
    mkSlots !i !sm !n = Map.findWithDefault 0 i sm : mkSlots (i + 1) sm n


alignUp :: Int -> Int -> Int
alignUp n a = (n + a - 1) .&. complement (a - 1)


writeString :: Builder -> T.Text -> IO Int
writeString b txt = do
  let !bytes = TE.encodeUtf8 txt
      !n = BS.length bytes
  prepForObject b (4 + n + 1) 4
  prependBS b (BS.snoc bytes 0)
  prependU32 b (fromIntegral n)
  currentUOff b


writeVectorOfOffsets :: Builder -> [Int] -> IO Int
writeVectorOfOffsets b targetUOffs = do
  let !n = length targetUOffs
  prepForObject b (4 + 4 * n) 4
  mapM_
    ( \t -> do
        cur <- currentUOff b
        prependU32 b (fromIntegral (cur + 4 - t))
    )
    (reverse targetUOffs)
  prependU32 b (fromIntegral n)
  currentUOff b


writeVectorOfStructs
  :: Builder
  -> Int
  -> Int
  -> [Builder -> IO ()]
  -> IO Int
writeVectorOfStructs b elemSize elemAlign writers = do
  let !n = length writers
      !totalBytes = 4 + elemSize * n
      !align = max 4 elemAlign
  prepForObject b totalBytes align
  mapM_ (\w -> w b) (reverse writers)
  prependU32 b (fromIntegral n)
  currentUOff b


writeVectorInt32 :: Builder -> [Int32] -> IO Int
writeVectorInt32 b xs = do
  let !n = length xs
  prepForObject b (4 + 4 * n) 4
  mapM_ (prependI32 b) (reverse xs)
  prependU32 b (fromIntegral n)
  currentUOff b


writeVectorInt64 :: Builder -> [Int64] -> IO Int
writeVectorInt64 b xs = do
  let !n = length xs
  prepForVector b (8 * n) 8
  mapM_ (prependI64 b) (reverse xs)
  prependU32 b (fromIntegral n)
  currentUOff b


prepForVector :: Builder -> Int -> Int -> IO ()
prepForVector b dataBytes align = do
  noteMinAlign b align
  !cur <- readIORef (bufSize b)
  let !pad = (negate cur) .&. (align - 1)
  prependBS b (BS.replicate pad 0)
  let _ = dataBytes
  pure ()