json-syntax-0.2.3.0: src/Json/Smile.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UnboxedTuples #-}
module Json.Smile
( -- * Encode JSON Document
encode
-- * Encode JSON Atoms
-- ** Integer
, encodeBigInteger
-- ** String
, encodeString
, encodeAsciiString
-- ** Key
, encodeKey
, encodeAsciiKey
) where
import Prelude hiding (Bool(..))
import Control.Monad.ST (ST)
import Control.Monad.ST.Run (runByteArrayST)
import Data.Bits (countLeadingZeros,complement,unsafeShiftR,(.&.),(.|.))
import Data.Bits (testBit)
import Data.Bytes.Builder (Builder)
import Data.Int (Int32)
import Data.Primitive (ByteArray,newByteArray)
import Data.Primitive (writeByteArray,byteArrayFromListN,sizeofByteArray)
import Data.Primitive (MutableByteArray(..),unsafeFreezeByteArray)
import Data.Primitive (readByteArray,copyMutableByteArray)
import Data.Text.Short (ShortText)
import Data.Word (Word8,Word32,Word64)
import Data.Word.Zigzag (toZigzag32,toZigzag64)
import GHC.Exts (RealWorld,Word#,State#)
import GHC.IO (IO(IO))
import GHC.Word (Word(..))
import Json (Value(..), Member(..))
import Numeric.Natural (Natural)
import System.IO.Unsafe (unsafeDupablePerformIO)
import qualified Arithmetic.Nat as Nat
import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Builder as B
import qualified Data.Bytes.Builder.Bounded as Bounded
import qualified Data.Bytes.Builder.Bounded.Unsafe as Unsafe
import qualified Data.ByteString.Short as SBS
import qualified Data.Number.Scientific as Sci
import qualified Data.Text.Short as TS
import qualified GHC.Exts as Exts
import qualified GHC.Num.BigNat as BN
import qualified GHC.Num.Integer as Integer
import qualified Prelude
-- | Encode a Json 'Value' to the Smile binary format.
-- This encoder does not produce backreferences.
encode :: Value -> Builder
{-# inline encode #-}
encode v0 = B.ascii4 ':' ')' '\n' '\x00' <> encodeNoHeader v0
-- The "rebuild" trick was adapted from the fast-builder library. It
-- results in a 2x performance gain on the twitter benchmark.
-- This function is marked noinline to ensure that its performance is
-- stable.
encodeNoHeader :: Value -> Builder
{-# noinline encodeNoHeader #-}
encodeNoHeader val = B.rebuild $ case val of
Object obj ->
B.word8 0xFA
<>
foldMap (\Member{key,value} -> encodeKey key <> encodeNoHeader value) obj
<>
B.word8 0xFB
Array arr -> B.word8 0xF8 <> foldMap encodeNoHeader arr <> B.word8 0xF9
String str -> encodeString str
Number x
| Just i32 <- Sci.toInt32 x
, -16 <= i32 && i32 <= 15
, w5 <- fromIntegral @Word32 @Word8 (toZigzag32 i32)
-> B.word8 (0xC0 + w5)
| Just i32 <- Sci.toInt32 x
-> B.fromBounded Nat.constant (Bounded.word8 0x24 `Bounded.append` vlqSmile64 (fromIntegral @Word32 @Word64 (toZigzag32 i32)))
| Just i64 <- Sci.toInt64 x
-> B.fromBounded Nat.constant (Bounded.word8 0x25 `Bounded.append` vlqSmile64 (toZigzag64 i64))
| otherwise -> Sci.withExposed encodeSmallDecimal encodeBigDecimal x
Null -> B.word8 0x21
False -> B.word8 0x22
True -> B.word8 0x23
encodeSmallDecimal :: Int -> Int -> Builder
encodeSmallDecimal !c !e = encodeBigDecimal (fromIntegral c) (fromIntegral e)
encodeBigDecimal :: Integer -> Integer -> Builder
encodeBigDecimal c e = case e of
0 -> encodeBigInteger c
_ -> B.word8 0x2A -- bigdecimal token tag
<> vlqSmile ( fromIntegral @Word32 @Natural
$ toZigzag32 scale)
<> vlqSmile (fromIntegral @Int @Natural $ sizeofByteArray raw) -- size of byte digits
<> B.sevenEightSmile (Bytes.fromByteArray raw) -- 7/8 encoding of byte digits
where
scale :: Int32
-- WARNING smile can't handle exponents outside int32_t, so this truncates
-- WARNING "scale" is what Java BigDecimal thinks, which is
-- negative of all mathematics since exponential notation was invented 💩
scale = fromIntegral @Integer @Int32 (-e)
raw = integerToBase256ByteArray c
-- | Encode a number using as SMILE @BigInteger@ token type (prefix @0x26@).
encodeBigInteger :: Integer -> Builder
encodeBigInteger n = B.word8 0x26
<> vlqSmile (fromIntegral @Int @Natural $ sizeofByteArray raw) -- size of byte digits
<> B.sevenEightSmile (Bytes.fromByteArray raw) -- 7/8 encoding of byte digits
where
!raw = integerToBase256ByteArray n
integerToBase256ByteArray :: Integer -> ByteArray
integerToBase256ByteArray c = if c == 0
then byteArrayFromListN 1 [0::Word8]
else case c of
Integer.IP bn -> unsafeDupablePerformIO $ do
let nDigits256 = fromIntegral @Word @Int (W# (BN.bigNatSizeInBase# 256## bn))
mut <- newByteArray nDigits256
let !(MutableByteArray mut#) = mut
!_ <- liftWordIO (BN.bigNatToMutableByteArray# bn mut# 0## 1# )
-- This is safe because Jp cannot have zero inside it.
w0 :: Word8 <- readByteArray mut 0
if testBit w0 7
then do
-- If the upper bit is 1, then we must introduce a leading
-- zero byte.
dst <- newByteArray (nDigits256 + 1)
writeByteArray dst 0 (0x00 :: Word8)
copyMutableByteArray dst 1 mut 0 nDigits256
unsafeFreezeByteArray dst
else unsafeFreezeByteArray mut
Integer.IN bn -> twosComplementBigNat bn
Integer.IS i -> case i Exts.># 0# of
1# -> encodePosWordBase256 (W# (Exts.int2Word# i))
_ -> encodeNegWordBase256 (W# (Exts.int2Word# i))
liftWordIO :: (State# RealWorld -> (# State# RealWorld, Word# #)) -> IO Word
{-# inline liftWordIO #-}
liftWordIO f = IO
(\s -> case f s of
(# s', w #) -> (# s', W# w #)
)
twosComplementBigNat :: BN.BigNat# -> ByteArray
twosComplementBigNat bn = unsafeDupablePerformIO $ do
let nDigits256 = fromIntegral @Word @Int (W# (BN.bigNatSizeInBase# 256## bn))
mut <- newByteArray nDigits256
let !(MutableByteArray mut#) = mut
!_ <- liftWordIO (BN.bigNatToMutableByteArray# bn mut# 0## 1# )
-- First, complement
let goComplement !ix = if ix >= 0
then do
w :: Word8 <- readByteArray mut ix
writeByteArray mut ix (complement w)
goComplement (ix - 1)
else pure ()
goComplement (nDigits256 - 1)
-- Second, add one
let goAddOne !ix = if ix >= 0
then do
w :: Word8 <- readByteArray mut ix
case w of
0xFF -> do
writeByteArray mut ix (0 :: Word8)
goAddOne (ix - 1)
_ -> writeByteArray mut ix (w + 1)
else pure ()
goAddOne (nDigits256 - 1)
leader :: Word8 <- readByteArray mut 0
if testBit leader 7
then unsafeFreezeByteArray mut
else do
dst <- newByteArray (nDigits256 + 1)
copyMutableByteArray dst 1 mut 0 nDigits256
writeByteArray dst 0 (0xFF :: Word8)
unsafeFreezeByteArray dst
-- Only works on 64-bit architectures.
-- Precondition: must be a bit pattern of a positive integer
encodePosWordBase256 :: Word -> ByteArray
encodePosWordBase256 !w = runByteArrayST $ do
-- If a positive number broken into its constituent bytes has
-- leading byte with MSB 1, then we want to have a zero byte
-- in front. That is, we want to produce:
--
-- 0000_0000 1101_1010 01010101
--
-- instead of
--
-- 1101_1010 01010101
--
-- Because the latter would be misinterpreted as a negative number.
let !total = quot (72 - countLeadingZeros w) 8
dst <- newByteArray total
let go !ix !acc = if ix >= 0
then do
writeByteArray dst ix (fromIntegral @Word @Word8 acc)
go (ix - 1) (unsafeShiftR acc 8)
else unsafeFreezeByteArray dst
go (total - 1) w
-- Same deal as encodePosWordBase256.
-- Andrew Martin: I am not sure why we need to subtract from 72
-- instead of 71. But if we change it, a bunch of tests fail.
encodeNegWordBase256 :: Word -> ByteArray
encodeNegWordBase256 !w = runByteArrayST $ do
let !total = quot (72 - countLeadingZeros (complement w)) 8
dst <- newByteArray total
let go !ix !acc = if ix >= 0
then do
writeByteArray dst ix (fromIntegral @Word @Word8 acc)
go (ix - 1) (unsafeShiftR acc 8)
else unsafeFreezeByteArray dst
go (total - 1) w
-- | Encode a string in which all characters are ASCII. This precondition
-- is not checked. Resulting output will be corrupt if this condition
-- is not satisfied.
encodeAsciiString :: ShortText -> Builder
encodeAsciiString !str
| n == 0 = B.word8 0x20
| n <= 64 = B.copyCons (0x40 + fromIntegral (n - 1)) (Bytes.fromShortByteString (TS.toShortByteString str))
| otherwise = B.word8 0xe0 <> B.shortTextUtf8 str <> B.word8 0xFC
where
n = SBS.length (TS.toShortByteString str)
-- | Encode a string.
encodeString :: ShortText -> Builder
encodeString !str = case SBS.length (TS.toShortByteString str) of
0 -> B.word8 0x20
n -> case TS.isAscii str of
Prelude.True
| n <= 64 -> B.copyCons (0x40 + fromIntegral (n - 1)) (Bytes.fromShortByteString (TS.toShortByteString str))
| otherwise -> B.word8 0xe0 <> B.shortTextUtf8 str <> B.word8 0xFC
Prelude.False
| n <= 65 -> B.copyCons (0x80 + fromIntegral (n - 2)) (Bytes.fromShortByteString (TS.toShortByteString str))
| otherwise -> B.word8 0xE4 <> B.shortTextUtf8 str <> B.word8 0xFC
-- | Encode a key.
encodeKey :: ShortText -> Builder
encodeKey !str = case SBS.length (TS.toShortByteString str) of
0 -> B.word8 0x20
n | n <= 64
&& TS.isAscii str
, w8 <- fromIntegral @Int @Word8 (n - 1)
-> B.copyCons (0x80 + w8) (Bytes.fromShortByteString (TS.toShortByteString str))
n | n < 56
, w8 <- fromIntegral @Int @Word8 (n - 2)
-> B.copyCons (0xC0 + w8) (Bytes.fromShortByteString (TS.toShortByteString str))
| otherwise -> B.word8 0x34 <> B.shortTextUtf8 str <> B.word8 0xFC
-- | Encode a key in which all characters are ASCII. This precondition
-- is not checked. Resulting output will be corrupt if this condition
-- is not satisfied.
encodeAsciiKey :: ShortText -> Builder
encodeAsciiKey str = case SBS.length (TS.toShortByteString str) of
0 -> B.word8 0x20
n | n <= 64
, w8 <- fromIntegral @Int @Word8 (n - 1)
-> B.word8 (0x80 + w8) <> B.shortTextUtf8 str
| otherwise -> B.word8 0x34 <> B.shortTextUtf8 str <> B.word8 0xFC
vlqSmile :: Natural -> Builder
vlqSmile n0 =
let (rest, lastBits) = take6bits n0
in loop rest <> B.word8 (lastBits .|. 0x80)
where
loop n
| n == 0 = mempty
| (rest, bits) <- take7bits n
= loop rest <> B.word8 bits
take7bits :: Natural -> (Natural, Word8)
take7bits n = (n `unsafeShiftR` 7, fromIntegral @Natural @Word8 n .&. 0x7F)
take6bits :: Natural -> (Natural, Word8)
take6bits n = (n `unsafeShiftR` 6, fromIntegral @Natural @Word8 n .&. 0x3F)
-- Precondition: input is not zero
vlqSmile64 :: Word64 -> Bounded.Builder 10
vlqSmile64 !n0 = Unsafe.construct $ \buf ix0 -> do
let !w0 = fromIntegral @Word64 @Word8 n0 .&. 0x3F
writeByteArray buf ix0 (0x80 .|. w0)
let !acc0 = n0 `unsafeShiftR` 6
let loop !acc !ix = case acc of
0 -> pure ix
_ -> do
let !w = fromIntegral @Word64 @Word8 acc .&. 0x7F
writeByteArray buf ix w
let !acc' = acc `unsafeShiftR` 7
loop acc' (ix + 1)
ix1 <- loop acc0 (ix0 + 1)
reverseBytes buf ix0 (ix1 - 1)
pure ix1
-- Reverse the bytes in the designated slice. This takes
-- an inclusive start offset and an inclusive end offset.
--
-- Copied from bytebuild
reverseBytes :: MutableByteArray s -> Int -> Int -> ST s ()
{-# inline reverseBytes #-}
reverseBytes arr begin end = go begin end where
go ixA ixB = if ixA < ixB
then do
a :: Word8 <- readByteArray arr ixA
b :: Word8 <- readByteArray arr ixB
writeByteArray arr ixA b
writeByteArray arr ixB a
go (ixA + 1) (ixB - 1)
else pure ()