packages feed

moonlight-delta-0.1.0.0: src-patch/Moonlight/Delta/Patch/Internal/IncrementalDigest.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}

module Moonlight.Delta.Patch.Internal.IncrementalDigest
  ( Digest128,
    DeltaHashDigest (..),
    DeltaHashBuildError (..),
    DeltaHashApplyError (..),
    IncrementalDigest (..),
    deltaHashEncodingVersion,
    digest128Encoding,
    digest128EncodingChunks,
  )
where

import Data.Bits (Bits (shiftR, (.&.), (.|.)))
import Data.Foldable qualified as Foldable
import Data.Kind (Type)
import Data.Word (Word64)
import GHC.Generics (Generic)
import Moonlight.Core
  ( SipHashState,
    SipKey,
    StableHashDigest (..),
    StableHashEncoding,
    stableHashEncodingDigest,
    stableHashEncodingLength,
    stableHashEncodingVersion,
    stableHashUpdateEncoding,
    sipHashFinalize,
    sipHashInit,
    sipHashUpdateWord8,
    sipHashUpdateWord64LE,
  )
import Moonlight.Delta.Patch.Internal.Types (ApplyError, Patch)
import Prelude

data DeltaHashDigest = DeltaHashDigest
  { deltaHashDigestLane0 :: {-# UNPACK #-} !Word64,
    deltaHashDigestLane1 :: {-# UNPACK #-} !Word64
  }
  deriving stock (Eq, Ord, Show, Read, Generic)

type Digest128 :: Type
type Digest128 = DeltaHashDigest

data DeltaHashBuildError key = DeltaHashKeyCollision
  { deltaHashCollisionDigest :: !StableHashDigest,
    deltaHashExistingKey :: !key,
    deltaHashIncomingKey :: !key
  }
  deriving stock (Eq, Ord, Show)

data DeltaHashApplyError key value
  = DeltaHashPatchRejected !(ApplyError key value)
  | DeltaHashUpdateRejected !(DeltaHashBuildError key)
  deriving stock (Eq, Ord, Show)

class IncrementalDigest (derived :: Type -> Type -> Type) where
  type IncrementalDigestError derived key value :: Type
  empty :: (key -> StableHashEncoding) -> (value -> StableHashEncoding) -> derived key value
  applyPatch ::
    (Ord key, Eq value) =>
    Patch key value ->
    derived key value ->
    Either (IncrementalDigestError derived key value) (derived key value)
  digest :: derived key value -> Digest128

deltaHashEncodingVersion :: Word64
deltaHashEncodingVersion = 2

digest128Encoding :: SipKey -> SipKey -> StableHashEncoding -> Digest128
digest128Encoding lane0Key lane1Key encoding =
  DeltaHashDigest (stableHashDigestWord lane0Key encoding) (stableHashDigestWord lane1Key encoding)

digest128EncodingChunks ::
  Foldable values =>
  SipKey ->
  SipKey ->
  (value -> StableHashEncoding) ->
  values value ->
  Digest128
digest128EncodingChunks lane0Key lane1Key project values =
  finalizeDigest128State (Foldable.foldl' absorbEncoding initialState values)
  where
    !chunkCount = fromIntegral (Foldable.length values)
    !initialState =
      Digest128HashState
        (framedChunksSeed lane0Key chunkCount)
        (framedChunksSeed lane1Key chunkCount)
    absorbEncoding (Digest128HashState lane0 lane1) value =
      let !encoding = project value
          !encodedLength = fromIntegral (stableHashEncodingLength encoding)
       in Digest128HashState
            (stableHashUpdateEncoding (sipHashUpdateCompactWord64 lane0 encodedLength) encoding)
            (stableHashUpdateEncoding (sipHashUpdateCompactWord64 lane1 encodedLength) encoding)

data Digest128HashState = Digest128HashState !SipHashState !SipHashState

finalizeDigest128State :: Digest128HashState -> Digest128
finalizeDigest128State (Digest128HashState lane0 lane1) =
  DeltaHashDigest (sipHashFinalize lane0) (sipHashFinalize lane1)

stableHashDigestWord :: SipKey -> StableHashEncoding -> Word64
stableHashDigestWord sipKey encoding =
  case stableHashEncodingDigest sipKey encoding of
    StableHashDigest digestWord -> digestWord

framedChunksSeed :: SipKey -> Word64 -> SipHashState
framedChunksSeed sipKey =
  sipHashUpdateCompactWord64
    (sipHashUpdateWord64LE (sipHashInit sipKey) stableHashEncodingVersion)

sipHashUpdateCompactWord64 :: SipHashState -> Word64 -> SipHashState
sipHashUpdateCompactWord64 state wordValue =
  let !byteValue = fromIntegral (wordValue .&. 0x7f)
      !remainingValue = wordValue `shiftR` 7
      !updatedState =
        sipHashUpdateWord8 state (if remainingValue == 0 then byteValue else byteValue .|. 0x80)
   in if remainingValue == 0
        then updatedState
        else sipHashUpdateCompactWord64 updatedState remainingValue