packages feed

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

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
-- Loop-local rebinding (state/cursor/coverage) is the engine idiom here; shadowing is deliberate.
{-# OPTIONS_GHC -Wno-name-shadowing #-}

module Moonlight.Delta.Patch.Internal.Builder
  ( Builder,
    newBuilder,
    appendTransition,
    appendPageCopy,
    finishBuilder,
    toPaged,
    toPagedMap,
  )
where

import Control.Monad.ST (ST, runST)
import Data.Bits (setBit, testBit)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Primitive.PrimArray
  ( MutablePrimArray,
    newPrimArray,
    readPrimArray,
    writePrimArray,
  )
import Data.Primitive.SmallArray
  ( SmallArray,
    SmallMutableArray,
    copySmallMutableArray,
    emptySmallArray,
    indexSmallArray,
    newSmallArray,
    readSmallArray,
    sizeofSmallArray,
    unsafeFreezeSmallArray,
    writeSmallArray,
  )
import Data.STRef
  ( STRef,
    modifySTRef',
    newSTRef,
    readSTRef,
    writeSTRef,
  )
import Data.Word (Word64)
import Moonlight.Delta.Patch.Internal.Cell
  ( cellAfterEndpoint,
    cellBeforeEndpoint,
  )
import Moonlight.Delta.Patch.Internal.Page
  ( ColumnView (..),
    columnView,
  )
import Moonlight.Delta.Patch.Internal.Types
  ( Endpoint (..),
    EndpointColumn (..),
    Cell (..),
    PatchKey,
    PatchValue,
    Patch (..),
    Page (..),
    buildKeyColumn,
    pageKeyAt,
    valueColumnAt,
    valueColumnFromArray,
  )
import Prelude

bufferCapacity :: Int
bufferCapacity = 96
{-# INLINE bufferCapacity #-}

sealedPrefixSize :: Int
sealedPrefixSize = 64
{-# INLINE sealedPrefixSize #-}

retainedSuffixSize :: Int
retainedSuffixSize = 32
{-# INLINE retainedSuffixSize #-}

data OpenArrays s key value = OpenArrays
  { openKeys :: !(SmallMutableArray s key),
    openBefore :: !(SmallMutableArray s (Endpoint value)),
    openAfter :: !(SmallMutableArray s (Endpoint value)),
    openCount :: !(MutablePrimArray s Int)
  }

data OpenBuffer s key value
  = OpenEmpty
  | OpenNonEmpty !(OpenArrays s key value)

data Builder s key value = Builder
  { totalCount :: !(MutablePrimArray s Int),
    openBuffer :: !(STRef s (OpenBuffer s key value)),
    pagesReverse :: !(STRef s [(key, Page key value)])
  }

newBuilder :: ST s (Builder s key value)
newBuilder = do
  totalCount <- newPrimArray 1
  writePrimArray totalCount 0 0
  openBuffer <- newSTRef OpenEmpty
  pagesReverse <- newSTRef []
  pure
    Builder
      { totalCount = totalCount,
        openBuffer = openBuffer,
        pagesReverse = pagesReverse
      }
{-# INLINE newBuilder #-}

appendTransition ::
  (PatchKey key, PatchValue value) =>
  Builder s key value ->
  key ->
  Endpoint value ->
  Endpoint value ->
  ST s ()
appendTransition builder@Builder {totalCount, openBuffer} !key !before !after = do
  readSTRef openBuffer >>= \case
    OpenEmpty -> do
      keys <- newSmallArray bufferCapacity key
      beforeValues <- newSmallArray bufferCapacity before
      afterValues <- newSmallArray bufferCapacity after
      count <- newPrimArray 1
      writePrimArray count 0 1
      writeSTRef
        openBuffer
        ( OpenNonEmpty
            OpenArrays
              { openKeys = keys,
                openBefore = beforeValues,
                openAfter = afterValues,
                openCount = count
              }
        )
    OpenNonEmpty arrays -> do
      count <- readPrimArray (openCount arrays) 0
      nextIndex <-
        if count == bufferCapacity
          then do
            sealPrefixAndRetainSuffix builder arrays
            pure retainedSuffixSize
          else pure count
      writeSmallArray (openKeys arrays) nextIndex key
      writeSmallArray (openBefore arrays) nextIndex before
      writeSmallArray (openAfter arrays) nextIndex after
      writePrimArray (openCount arrays) 0 (nextIndex + 1)
  total <- readPrimArray totalCount 0
  writePrimArray totalCount 0 (total + 1)
{-# INLINE appendTransition #-}

sealPrefixAndRetainSuffix :: (PatchKey key, PatchValue value) => Builder s key value -> OpenArrays s key value -> ST s ()
sealPrefixAndRetainSuffix builder arrays = do
  commitRange builder arrays 0 sealedPrefixSize
  shiftRetainedSuffix arrays
  writePrimArray (openCount arrays) 0 retainedSuffixSize
{-# INLINE sealPrefixAndRetainSuffix #-}

shiftRetainedSuffix :: OpenArrays s key value -> ST s ()
shiftRetainedSuffix OpenArrays {openKeys, openBefore, openAfter} =
  go 0
  where
    go !index
      | index == retainedSuffixSize =
          pure ()
      | otherwise = do
          let !sourceIndex = sealedPrefixSize + index
          key <- readSmallArray openKeys sourceIndex
          before <- readSmallArray openBefore sourceIndex
          after <- readSmallArray openAfter sourceIndex
          writeSmallArray openKeys index key
          writeSmallArray openBefore index before
          writeSmallArray openAfter index after
          go (index + 1)
{-# INLINE shiftRetainedSuffix #-}

commitRange :: (PatchKey key, PatchValue value) => Builder s key value -> OpenArrays s key value -> Int -> Int -> ST s ()
commitRange Builder {pagesReverse} OpenArrays {openKeys, openBefore, openAfter} !start !rangeLength =
  if rangeLength <= 0
    then pure ()
    else do
      let !maximumIndex = start + rangeLength - 1
          !prefixLength = rangeLength - 1
      maximumKey <- readSmallArray openKeys maximumIndex
      prefixKeys <- freezeSlice openKeys start prefixLength
      beforeColumn <- compressColumn openBefore start rangeLength
      afterColumn <- compressColumn openAfter start rangeLength
      let !page =
            Page
              { pageCount = rangeLength,
                pagePrefixKeys = buildKeyColumn prefixKeys,
                pageBeforeColumn = beforeColumn,
                pageAfterColumn = afterColumn
              }
      modifySTRef' pagesReverse ((maximumKey, page) :)
{-# INLINE commitRange #-}

freezeSlice :: SmallMutableArray s value -> Int -> Int -> ST s (SmallArray value)
freezeSlice _ _ 0 =
  pure emptySmallArray
freezeSlice source !start !rangeLength = do
  seed <- readSmallArray source start
  target <- newSmallArray rangeLength seed
  copySmallMutableArray target 0 source start rangeLength
  unsafeFreezeSmallArray target
{-# INLINE freezeSlice #-}

compressColumn :: forall s value. PatchValue value => SmallMutableArray s (Endpoint value) -> Int -> Int -> ST s (EndpointColumn value)
compressColumn source !start !rangeLength = do
  (mask, presentCount, firstPresent) <- scan 0 0 0 Nothing
  values <-
    case firstPresent of
      Nothing ->
        pure emptySmallArray
      Just seed -> do
        target <- newSmallArray presentCount seed
        fill target 0 0
        unsafeFreezeSmallArray target
  if presentCount == rangeLength
    then pure (AllPresent (valueColumnFromArray values))
    else pure (Presence mask (valueColumnFromArray values))
  where
    scan :: Int -> Word64 -> Int -> Maybe value -> ST s (Word64, Int, Maybe value)
    scan !logicalIndex !mask !presentCount !firstPresent
      | logicalIndex == rangeLength =
          pure (mask, presentCount, firstPresent)
      | otherwise = do
          endpoint <- readSmallArray source (start + logicalIndex)
          case endpoint of
            EndpointAbsent ->
              scan (logicalIndex + 1) mask presentCount firstPresent
            EndpointPresent value ->
              scan
                (logicalIndex + 1)
                (setBit mask logicalIndex)
                (presentCount + 1)
                (case firstPresent of Nothing -> Just value; Just existing -> Just existing)

    fill :: SmallMutableArray s value -> Int -> Int -> ST s ()
    fill target !logicalIndex !packedIndex
      | logicalIndex == rangeLength =
          pure ()
      | otherwise = do
          endpoint <- readSmallArray source (start + logicalIndex)
          case endpoint of
            EndpointAbsent ->
              fill target (logicalIndex + 1) packedIndex
            EndpointPresent value -> do
              writeSmallArray target packedIndex value
              fill target (logicalIndex + 1) (packedIndex + 1)
{-# INLINE compressColumn #-}

appendPageCopy :: (PatchKey key, PatchValue value) => Builder s key value -> key -> Page key value -> ST s ()
appendPageCopy builder maximumKey page =
  case
      ( columnView (pageCount page) (pageBeforeColumn page),
        columnView (pageCount page) (pageAfterColumn page)
      )
    of
      (ColumnView beforeMask beforeValues, ColumnView afterMask afterValues) ->
        go 0 0 0 beforeMask beforeValues afterMask afterValues
  where
    go !logicalIndex !beforePackedIndex !afterPackedIndex !beforeMask !beforeValues !afterMask !afterValues
      | logicalIndex == pageCount page =
          pure ()
      | otherwise = do
          let !key = pageKeyAt maximumKey page logicalIndex
              !beforePresent = testBit beforeMask logicalIndex
              !afterPresent = testBit afterMask logicalIndex
              !before =
                if beforePresent
                  then EndpointPresent (valueColumnAt beforeValues beforePackedIndex)
                  else EndpointAbsent
              !after =
                if afterPresent
                  then EndpointPresent (valueColumnAt afterValues afterPackedIndex)
                  else EndpointAbsent
              !nextBeforePackedIndex =
                if beforePresent
                  then beforePackedIndex + 1
                  else beforePackedIndex
              !nextAfterPackedIndex =
                if afterPresent
                  then afterPackedIndex + 1
                  else afterPackedIndex
          appendTransition builder key before after
          go
            (logicalIndex + 1)
            nextBeforePackedIndex
            nextAfterPackedIndex
            beforeMask
            beforeValues
            afterMask
            afterValues
{-# INLINE appendPageCopy #-}

finishBuilderPages ::
  (PatchKey key, PatchValue value) =>
  Builder s key value ->
  ST s (Int, Map key (Page key value))
finishBuilderPages builder@Builder {totalCount, openBuffer, pagesReverse} = do
  readSTRef openBuffer >>= \case
    OpenEmpty ->
      pure ()
    OpenNonEmpty arrays -> do
      count <- readPrimArray (openCount arrays) 0
      if count <= sealedPrefixSize
        then commitRange builder arrays 0 count
        else do
          let !leftCount = count - retainedSuffixSize
          commitRange builder arrays 0 leftCount
          commitRange builder arrays leftCount retainedSuffixSize
  totalCount <- readPrimArray totalCount 0
  pagesReverse <- readSTRef pagesReverse
  pure (totalCount, Map.fromDistinctAscList (reverse pagesReverse))
{-# INLINE finishBuilderPages #-}

finishBuilder :: (PatchKey key, PatchValue value) => Builder s key value -> ST s (Patch key value)
finishBuilder builder = do
  (totalCount, pages) <- finishBuilderPages builder
  pure (PagedPatch totalCount pages)
{-# INLINE finishBuilder #-}

promoteCells ::
  (PatchKey key, PatchValue value) =>
  SmallArray (Cell key value) ->
  (Int, Map key (Page key value))
promoteCells cells =
  runST $ do
    builder <- newBuilder
    appendSmallCells builder cells 0 (sizeofSmallArray cells)
    finishBuilderPages builder
{-# INLINABLE promoteCells #-}

appendSmallCells ::
  (PatchKey key, PatchValue value) =>
  Builder s key value ->
  SmallArray (Cell key value) ->
  Int ->
  Int ->
  ST s ()
appendSmallCells builder cells !index !count
  | index == count =
      pure ()
  | otherwise =
      case indexSmallArray cells index of
        Cell key cell -> do
          appendTransition builder key (cellBeforeEndpoint cell) (cellAfterEndpoint cell)
          appendSmallCells builder cells (index + 1) count
{-# INLINABLE appendSmallCells #-}

toPaged :: (PatchKey key, PatchValue value) => Patch key value -> Patch key value
toPaged patch =
  case patch of
    PagedPatch _count _pages ->
      patch
    SmallPatch cells ->
      let (!count, !pages) = promoteCells cells
       in PagedPatch count pages
{-# INLINABLE toPaged #-}

toPagedMap :: (PatchKey key, PatchValue value) => Patch key value -> Map key (Page key value)
toPagedMap patch =
  case patch of
    PagedPatch _count pages ->
      pages
    SmallPatch cells ->
      snd (promoteCells cells)
{-# INLINABLE toPagedMap #-}