packages feed

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

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# 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.Page
  ( ColumnView (..),
    validateAlignedPageBoundary,
    invertPage,
    replaceRecordedPage,
    replacePageEntryAfter,
    columnView,
    columnMaybeAt,
    columnEndpointFromView,
    advancePackedIndex,
    rowCellAt,
    pageMinimumKey,
    pageLookupIndex,
    pageForKey,
    pageForInsertion,
    minimumKey,
    maximumKey,
    pageKeyAt,
  )
where

import Data.Bits (Bits (complement, shiftL, testBit, (.&.), (.|.)))
import Data.List qualified as List
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Primitive.SmallArray
  ( emptySmallArray,
    indexSmallArray,
    sizeofSmallArray,
    smallArrayFromList,
  )
import Data.Word (Word64)
import Moonlight.Delta.Patch.Internal.Cell
import Moonlight.Delta.Patch.Internal.Types
import Prelude

data ColumnView value = ColumnView
  { columnViewMask :: {-# UNPACK #-} !Word64,
    columnViewValues :: !(ValueColumn value)
  }

validateAlignedPageBoundary ::
  forall key value error.
  (Ord key, Eq value) =>
  (key -> Endpoint value -> Endpoint value -> error) ->
  key ->
  Page key value ->
  EndpointColumn value ->
  key ->
  Page key value ->
  EndpointColumn value ->
  BoundaryResult error
validateAlignedPageBoundary makeError actualMaximum actualPage actualColumn requiredMaximum requiredPage requiredColumn
  | pageCount actualPage /= pageCount requiredPage =
      PageBoundaryDiverged
  | otherwise =
      case (firstPageKeyMismatch actualMaximum actualPage requiredMaximum requiredPage, firstEndpointMismatch count actualColumn requiredColumn) of
        (Nothing, Nothing) ->
          PageBoundaryMatched
        (Just _keyMismatch, Nothing) ->
          PageBoundaryDiverged
        (Nothing, Just endpointMismatch) ->
          endpointFailure endpointMismatch
        (Just keyMismatch, Just endpointMismatch)
          | keyMismatch <= endpointMismatch ->
              PageBoundaryDiverged
          | otherwise ->
              endpointFailure endpointMismatch
  where
    !count = pageCount actualPage

    endpointFailure endpointMismatch =
      PageBoundaryRejected
        ( makeError
            (pageKeyAt requiredMaximum requiredPage endpointMismatch)
            (endpointAtByScan actualColumn endpointMismatch)
            (endpointAtByScan requiredColumn endpointMismatch)
        )
{-# INLINABLE validateAlignedPageBoundary #-}

firstPageKeyMismatch ::
  Ord key =>
  key ->
  Page key leftValue ->
  key ->
  Page key rightValue ->
  Maybe Int
firstPageKeyMismatch leftMaximum leftPage rightMaximum rightPage =
  case firstPrefixKeyMismatch (pagePrefixKeys leftPage) (pagePrefixKeys rightPage) of
    Just mismatch ->
      Just mismatch
    Nothing
      | compare leftMaximum rightMaximum == EQ ->
          Nothing
      | otherwise ->
          Just (leftCount - 1)
  where
    !leftCount = pageCount leftPage

{-# INLINE firstPageKeyMismatch #-}

firstPrefixKeyMismatch :: Ord key => KeyColumn key -> KeyColumn key -> Maybe Int
firstPrefixKeyMismatch left right
  | leftCount /= rightCount =
      Just 0
  | otherwise =
      case (left, right) of
        (IntRangeKeys leftStart leftSize, IntRangeKeys rightStart rightSize)
          | leftSize /= rightSize ->
              Just 0
          | leftStart == rightStart ->
              Nothing
          | otherwise ->
              Just 0
        (IntAffineKeys leftStart leftStep leftSize, IntAffineKeys rightStart rightStep rightSize)
          | leftSize /= rightSize ->
              Just 0
          | leftStart /= rightStart ->
              Just 0
          | leftSize <= 1 || leftStep == rightStep ->
              Nothing
          | otherwise ->
              Just 1
        (IntRangeKeys leftStart leftSize, IntAffineKeys rightStart rightStep rightSize)
          | leftSize /= rightSize ->
              Just 0
          | leftStart /= rightStart ->
              Just 0
          | leftSize <= 1 || rightStep == 1 ->
              Nothing
          | otherwise ->
              Just 1
        (IntAffineKeys leftStart leftStep leftSize, IntRangeKeys rightStart rightSize)
          | leftSize /= rightSize ->
              Just 0
          | leftStart /= rightStart ->
              Just 0
          | leftSize <= 1 || leftStep == 1 ->
              Nothing
          | otherwise ->
              Just 1
        _ ->
          scan 0
  where
    !leftCount = keyColumnCount left
    !rightCount = keyColumnCount right

    scan !index
      | index == leftCount =
          Nothing
      | compare (keyColumnAt left index) (keyColumnAt right index) == EQ =
          scan (index + 1)
      | otherwise =
          Just index
{-# INLINABLE firstPrefixKeyMismatch #-}

firstEndpointMismatch :: Eq value => Int -> EndpointColumn value -> EndpointColumn value -> Maybe Int
firstEndpointMismatch count left right =
  case (columnView count left, columnView count right) of
    (leftView, rightView) ->
      scan leftView rightView 0 0 0
  where
    scan !leftView !rightView !index !leftPackedIndex !rightPackedIndex
      | index == count =
          Nothing
      | endpointsEqualValue
          (columnEndpointFromView leftView index leftPackedIndex)
          (columnEndpointFromView rightView index rightPackedIndex) =
          scan
            leftView
            rightView
            (index + 1)
            (advancePackedIndex leftView index leftPackedIndex)
            (advancePackedIndex rightView index rightPackedIndex)
      | otherwise =
          Just index
{-# INLINABLE firstEndpointMismatch #-}


replaceRecordedPage :: Ord key => key -> key -> Page key value -> Map key (Page key value) -> Map key (Page key value)
replaceRecordedPage oldMaximum updatedMaximum updatedPage =
  Map.insert updatedMaximum updatedPage . Map.delete oldMaximum
{-# INLINE replaceRecordedPage #-}

replacePageEntryAfter :: (PatchKey key, PatchValue value) => key -> Endpoint value -> Int -> key -> Page key value -> (key, Page key value)
replacePageEntryAfter key after rowIndex maximumKey page =
  let !count = pageCount page
      (!updatedMaximum, !updatedPrefixKeys) = replacePageKeyAt key rowIndex maximumKey page
      !updatedAfterColumn = replaceColumnEndpointAt count rowIndex after (pageAfterColumn page)
   in ( updatedMaximum,
        page
          { pagePrefixKeys = updatedPrefixKeys,
            pageAfterColumn = updatedAfterColumn
          }
      )
{-# INLINABLE replacePageEntryAfter #-}

replacePageKeyAt :: PatchKey key => key -> Int -> key -> Page key value -> (key, KeyColumn key)
replacePageKeyAt key rowIndex maximumKey page =
  if rowIndex + 1 == pageCount page
    then (key, pagePrefixKeys page)
    else (maximumKey, rebuildPrefixKeys key rowIndex page)
{-# INLINE replacePageKeyAt #-}

rebuildPrefixKeys :: PatchKey key => key -> Int -> Page key value -> KeyColumn key
rebuildPrefixKeys replacement replacementIndex page =
  buildKeyColumn (smallArrayFromList (collect 0))
  where
    !count = pageCount page - 1

    collect !index
      | index == count =
          []
      | index == replacementIndex =
          replacement : collect (index + 1)
      | otherwise =
          keyColumnAt (pagePrefixKeys page) index : collect (index + 1)
{-# INLINE rebuildPrefixKeys #-}

replaceColumnEndpointAt :: PatchValue value => Int -> Int -> Endpoint value -> EndpointColumn value -> EndpointColumn value
replaceColumnEndpointAt count replacementIndex replacement column =
  columnFromEndpoints count (collect 0 0)
  where
    !view = columnView count column

    collect !index !packedIndex
      | index == count =
          []
      | index == replacementIndex =
          let !nextPackedIndex = advancePackedIndex view index packedIndex
           in replacement : collect (index + 1) nextPackedIndex
      | otherwise =
          let !endpoint = columnEndpointFromView view index packedIndex
              !nextPackedIndex = advancePackedIndex view index packedIndex
           in endpoint : collect (index + 1) nextPackedIndex
{-# INLINE replaceColumnEndpointAt #-}

invertPage :: Page key value -> Page key value
invertPage page =
  page
    { pageBeforeColumn = pageAfterColumn page,
      pageAfterColumn = pageBeforeColumn page
    }
{-# INLINE invertPage #-}

columnFromEndpoints :: forall value. PatchValue value => Int -> [Endpoint value] -> EndpointColumn value
columnFromEndpoints count endpoints =
  let (!mask, !valuesReversed) =
        List.foldl' collectEndpoint (0 :: Word64, []) (List.zip [0 :: Int ..] endpoints)
      !values = List.reverse valuesReversed
   in if mask == lowBits count
        then AllPresent (valueColumnFromList values)
        else Presence mask (valueColumnFromList values)
  where
    collectEndpoint :: (Word64, [value]) -> (Int, Endpoint value) -> (Word64, [value])
    collectEndpoint (!mask, !valuesReversed) (index, endpoint) =
      case endpoint of
        EndpointAbsent ->
          (mask, valuesReversed)
        EndpointPresent value ->
          (mask .|. bitAt index, value : valuesReversed)
{-# INLINE columnFromEndpoints #-}

valueColumnFromList :: PatchValue value => [value] -> ValueColumn value
valueColumnFromList values =
  case values of
    [] ->
      DenseValues emptySmallArray
    _ ->
      valueColumnFromArray (smallArrayFromList values)
{-# INLINABLE valueColumnFromList #-}


bitAt :: Int -> Word64
bitAt index =
  (1 :: Word64) `shiftL` index
{-# INLINE bitAt #-}

lowBits :: Int -> Word64
lowBits count
  | count <= 0 =
      0
  | count >= pageCapacity =
      complement 0
  | otherwise =
      bitAt count - 1
{-# INLINE lowBits #-}


columnView :: Int -> EndpointColumn value -> ColumnView value
columnView !count column =
  case column of
    AllPresent values ->
      ColumnView (lowBits count) values
    Presence mask values ->
      ColumnView (mask .&. lowBits count) values
{-# INLINE columnView #-}

columnMaybeAt :: ColumnView value -> Int -> Int -> Maybe value
columnMaybeAt (ColumnView mask values) !logicalIndex !packedIndex =
  if testBit mask logicalIndex
    then Just (valueColumnAt values packedIndex)
    else Nothing
{-# INLINE columnMaybeAt #-}

columnEndpointFromView :: ColumnView value -> Int -> Int -> Endpoint value
columnEndpointFromView view logicalIndex packedIndex =
  case columnMaybeAt view logicalIndex packedIndex of
    Nothing -> EndpointAbsent
    Just value -> EndpointPresent value
{-# INLINE columnEndpointFromView #-}


advancePackedIndex :: ColumnView value -> Int -> Int -> Int
advancePackedIndex (ColumnView mask _values) logicalIndex packedIndex =
  if testBit mask logicalIndex
    then packedIndex + 1
    else packedIndex
{-# INLINE advancePackedIndex #-}


endpointAtByScan :: EndpointColumn value -> Int -> Endpoint value
endpointAtByScan column target =
  case columnView (target + 1) column of
    view -> go view 0 0
  where
    go view !index !packedIndex
      | index == target =
          columnEndpointFromView view index packedIndex
      | otherwise =
          go view (index + 1) (advancePackedIndex view index packedIndex)
{-# INLINE endpointAtByScan #-}

endpointsEqualValue :: Eq value => Endpoint value -> Endpoint value -> Bool
endpointsEqualValue left right =
  case (left, right) of
    (EndpointAbsent, EndpointAbsent) ->
      True
    (EndpointPresent leftValue, EndpointPresent rightValue) ->
      leftValue == rightValue
    _ ->
      False
{-# INLINE endpointsEqualValue #-}

rowCellAt :: Page key value -> Int -> CellPatch value
rowCellAt page index =
  cellFromEndpointPair
    (endpointAtByScan (pageBeforeColumn page) index)
    (endpointAtByScan (pageAfterColumn page) index)
{-# INLINE rowCellAt #-}

pageMinimumKey :: key -> Page key value -> key
pageMinimumKey maximumKey page =
  pageKeyAt maximumKey page 0
{-# INLINE pageMinimumKey #-}

pageLookupIndex :: Ord key => key -> key -> Page key value -> Maybe Int
pageLookupIndex target maximumKey page =
  search 0 (pageCount page - 1)
  where
    search low high
      | low > high =
          Nothing
      | otherwise =
          let !middle = (low + high) `quot` 2
              !middleKey = pageKeyAt maximumKey page middle
           in case compare target middleKey of
                LT -> search low (middle - 1)
                GT -> search (middle + 1) high
                EQ -> Just middle
{-# INLINABLE pageLookupIndex #-}

pageForKey :: Ord key => key -> Map key (Page key value) -> Maybe (key, Page key value)
pageForKey key pages =
  Map.lookupGE key pages
{-# INLINE pageForKey #-}

pageForInsertion :: Ord key => key -> Map key (Page key value) -> Maybe (key, Page key value)
pageForInsertion key pages =
  case Map.lookupGE key pages of
    Just pageEntry -> Just pageEntry
    Nothing -> Map.lookupMax pages
{-# INLINE pageForInsertion #-}

minimumKey :: Patch key value -> Maybe key
minimumKey patch =
  case patch of
    SmallPatch cells
      | sizeofSmallArray cells == 0 ->
          Nothing
      | otherwise ->
          case indexSmallArray cells 0 of
            Cell key _cell ->
              Just key
    PagedPatch _count pages ->
      case Map.lookupMin pages of
        Nothing ->
          Nothing
        Just (maximumKey, page) ->
          Just (pageKeyAt maximumKey page 0)
{-# INLINE minimumKey #-}

maximumKey :: Patch key value -> Maybe key
maximumKey patch =
  case patch of
    SmallPatch cells
      | sizeofSmallArray cells == 0 ->
          Nothing
      | otherwise ->
          case indexSmallArray cells (sizeofSmallArray cells - 1) of
            Cell key _cell ->
              Just key
    PagedPatch _count pages ->
      fmap fst (Map.lookupMax pages)
{-# INLINE maximumKey #-}