packages feed

moonlight-core-0.1.0.0: src-solver/Moonlight/Core/Fixpoint/Dense/Internal/Scratch.hs

-- | The graph-agnostic mutable workspace for adaptive reachability: the
-- double-buffered sparse/dense frontier scratch arena, generation-stamped visit
-- marks, frontier folds/membership, and the strict monadic fold primitive. This
-- is the rawest ST stratum — it names neither 'Csr' nor 'GraphSnapshot'.
module Moonlight.Core.Fixpoint.Dense.Internal.Scratch
  ( ScratchSide (..),
    Frontier (..),
    ReachabilityScratch,
    newReachabilityScratch,
    nextReachabilityGeneration,
    markVectorFresh,
    appendFreshTarget,
    appendSparseBuffer,
    writeDenseFrontierM,
    flipScratchSide,
    markVisited,
    isMarked,
    visitedMarksToIntSet,
    strictFoldM,
    frontierEmpty,
    frontierSize,
    frontierMemberM,
    frontierContainsAnyM,
    frontierFoldM',
  )
where

import Control.Monad.ST (ST)
import Data.Bits (setBit, testBit)
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Kind (Type)
import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
import Data.Vector.Unboxed (Vector)
import Data.Vector.Unboxed qualified as U
import Data.Vector.Unboxed.Mutable qualified as UM
import Data.Word (Word32, Word64)
import Moonlight.Core.Fixpoint.Dense.Internal.AdaptiveIntSet (wordBits, wordCountForSize)
import Prelude

data ScratchSide
  = ScratchA
  | ScratchB
  deriving stock (Eq, Show)

data Frontier
  = SparseFrontier !ScratchSide !Int
  | DenseFrontier !ScratchSide !Int
  deriving stock (Eq, Show)

type ReachabilityScratch :: Type -> Type
data ReachabilityScratch state = ReachabilityScratch
  { visitedGeneration :: !(UM.MVector state Word32),
    currentGeneration :: !(STRef state Word32),
    sparseA :: !(UM.MVector state Int),
    sparseB :: !(UM.MVector state Int),
    denseA :: !(UM.MVector state Word64),
    denseB :: !(UM.MVector state Word64)
  }

newReachabilityScratch :: Int -> ST state (ReachabilityScratch state)
newReachabilityScratch vertexCount = do
  visited <- UM.replicate (max 0 vertexCount) 0
  firstSparse <- UM.replicate (max 0 vertexCount) 0
  secondSparse <- UM.replicate (max 0 vertexCount) 0
  firstDense <- UM.replicate (wordCountForSize vertexCount) 0
  secondDense <- UM.replicate (wordCountForSize vertexCount) 0
  generation <- newSTRef 0
  pure
    ReachabilityScratch
      { visitedGeneration = visited,
        currentGeneration = generation,
        sparseA = firstSparse,
        sparseB = secondSparse,
        denseA = firstDense,
        denseB = secondDense
      }
{-# INLINE newReachabilityScratch #-}

nextReachabilityGeneration :: ReachabilityScratch state -> ST state Word32
nextReachabilityGeneration scratch = do
  previous <- readSTRef (currentGeneration scratch)
  let next = previous + 1
  if next == 0
    then do
      UM.set (visitedGeneration scratch) 0
      writeSTRef (currentGeneration scratch) 1
      pure 1
    else do
      writeSTRef (currentGeneration scratch) next
      pure next
{-# INLINE nextReachabilityGeneration #-}

frontierEmpty :: Frontier -> Bool
frontierEmpty frontier =
  case frontier of
    SparseFrontier _ frontierLength -> frontierLength <= 0
    DenseFrontier _ frontierLength -> frontierLength <= 0
{-# INLINE frontierEmpty #-}

frontierSize :: Frontier -> Int
frontierSize frontier =
  case frontier of
    SparseFrontier _ frontierLength -> frontierLength
    DenseFrontier _ frontierLength -> frontierLength
{-# INLINE frontierSize #-}

frontierMemberM :: ReachabilityScratch state -> Frontier -> Int -> ST state Bool
frontierMemberM scratch frontier key =
  case frontier of
    SparseFrontier side frontierLength ->
      sparseFrontierMemberM (sparseBufferFor side scratch) frontierLength key
    DenseFrontier side _ ->
      denseFrontierMemberM (denseBufferFor side scratch) key
{-# INLINE frontierMemberM #-}

frontierContainsAnyM :: ReachabilityScratch state -> Frontier -> Vector Int -> ST state Bool
frontierContainsAnyM scratch frontier =
  U.foldr
    ( \key remaining -> do
        found <- frontierMemberM scratch frontier key
        if found then pure True else remaining
    )
    (pure False)
{-# INLINE frontierContainsAnyM #-}

frontierFoldM' :: ReachabilityScratch state -> (result -> Int -> ST state result) -> result -> Frontier -> ST state result
frontierFoldM' scratch step initial frontier =
  case frontier of
    SparseFrontier side frontierLength ->
      sparseFrontierFoldM' (sparseBufferFor side scratch) frontierLength step initial
    DenseFrontier side _ ->
      denseFrontierFoldM' (denseBufferFor side scratch) step initial
{-# INLINE frontierFoldM' #-}

markVectorFresh :: ReachabilityScratch state -> Word32 -> ScratchSide -> Vector Int -> ST state Int
markVectorFresh scratch generation targetSide =
  U.foldM' mark 0
  where
    mark freshLength key = do
      marked <- markFresh scratch generation key
      if marked
        then appendSparseBuffer scratch targetSide freshLength key
        else pure freshLength
{-# INLINE markVectorFresh #-}

appendFreshTarget :: ReachabilityScratch state -> Word32 -> ScratchSide -> Int -> Int -> ST state Int
appendFreshTarget scratch generation targetSide freshLength target = do
  marked <- markFresh scratch generation target
  if marked
    then appendSparseBuffer scratch targetSide freshLength target
    else pure freshLength
{-# INLINE appendFreshTarget #-}

appendSparseBuffer :: ReachabilityScratch state -> ScratchSide -> Int -> Int -> ST state Int
appendSparseBuffer scratch targetSide index value
  | index >= UM.length target = pure index
  | otherwise = do
      UM.write target index value
      pure (index + 1)
  where
    target =
      sparseBufferFor targetSide scratch
{-# INLINE appendSparseBuffer #-}

writeDenseFrontierM :: ReachabilityScratch state -> ScratchSide -> ScratchSide -> Int -> ST state ()
writeDenseFrontierM scratch sourceSide targetSide frontierLength = do
  UM.set target 0
  strictFoldM
    ( \() index -> do
        value <- UM.read source index
        setDenseBit target value
    )
    ()
    [0 .. frontierLength - 1]
  where
    source =
      sparseBufferFor sourceSide scratch
    target =
      denseBufferFor targetSide scratch
{-# INLINE writeDenseFrontierM #-}

sparseFrontierFoldM' :: UM.MVector state Int -> Int -> (result -> Int -> ST state result) -> result -> ST state result
sparseFrontierFoldM' sparse frontierLength step initial =
  strictFoldM
    ( \acc index -> do
        value <- UM.read sparse index
        step acc value
    )
    initial
    [0 .. frontierLength - 1]
{-# INLINE sparseFrontierFoldM' #-}

denseFrontierFoldM' :: UM.MVector state Word64 -> (result -> Int -> ST state result) -> result -> ST state result
denseFrontierFoldM' dense step initial =
  strictFoldM foldWord initial [0 .. UM.length dense - 1]
  where
    foldWord acc wordIndex = do
      word <- UM.read dense wordIndex
      strictFoldM
        ( \current bitIndex ->
            if testBit word bitIndex
              then step current (wordIndex * wordBits + bitIndex)
              else pure current
        )
        acc
        [0 .. wordBits - 1]
{-# INLINE denseFrontierFoldM' #-}

sparseFrontierMemberM :: UM.MVector state Int -> Int -> Int -> ST state Bool
sparseFrontierMemberM sparse frontierLength key =
  foldr
    ( \index remaining -> do
        found <- (== key) <$> UM.read sparse index
        if found then pure True else remaining
    )
    (pure False)
    [0 .. frontierLength - 1]
{-# INLINE sparseFrontierMemberM #-}

denseFrontierMemberM :: UM.MVector state Word64 -> Int -> ST state Bool
denseFrontierMemberM dense key
  | key < 0 = pure False
  | wordIndex >= UM.length dense = pure False
  | otherwise = do
      word <- UM.read dense wordIndex
      pure (testBit word bitIndex)
  where
    wordIndex =
      key `quot` wordBits
    bitIndex =
      key `rem` wordBits
{-# INLINE denseFrontierMemberM #-}

sparseBufferFor :: ScratchSide -> ReachabilityScratch state -> UM.MVector state Int
sparseBufferFor side scratch =
  case side of
    ScratchA -> sparseA scratch
    ScratchB -> sparseB scratch
{-# INLINE sparseBufferFor #-}

denseBufferFor :: ScratchSide -> ReachabilityScratch state -> UM.MVector state Word64
denseBufferFor side scratch =
  case side of
    ScratchA -> denseA scratch
    ScratchB -> denseB scratch
{-# INLINE denseBufferFor #-}

flipScratchSide :: ScratchSide -> ScratchSide
flipScratchSide side =
  case side of
    ScratchA -> ScratchB
    ScratchB -> ScratchA
{-# INLINE flipScratchSide #-}

markFresh :: ReachabilityScratch state -> Word32 -> Int -> ST state Bool
markFresh scratch generation key
  | key < 0 = pure False
  | key >= UM.length (visitedGeneration scratch) = pure False
  | otherwise = do
      seen <- isMarked scratch generation key
      if seen
        then pure False
        else markVisited scratch generation key *> pure True
{-# INLINE markFresh #-}

markVisited :: ReachabilityScratch state -> Word32 -> Int -> ST state ()
markVisited scratch generation key =
  UM.write (visitedGeneration scratch) key generation
{-# INLINE markVisited #-}

isMarked :: ReachabilityScratch state -> Word32 -> Int -> ST state Bool
isMarked scratch generation key =
  (== generation) <$> UM.read (visitedGeneration scratch) key
{-# INLINE isMarked #-}

visitedMarksToIntSet :: ReachabilityScratch state -> Word32 -> ST state IntSet
visitedMarksToIntSet scratch generation =
  IntSet.fromDistinctAscList . reverse
    <$> strictFoldM step [] [0 .. UM.length (visitedGeneration scratch) - 1]
  where
    step keys key = do
      seen <- isMarked scratch generation key
      pure (if seen then key : keys else keys)
{-# INLINE visitedMarksToIntSet #-}

setDenseBit :: UM.MVector state Word64 -> Int -> ST state ()
setDenseBit bitmap key
  | key < 0 = pure ()
  | wordIndex >= UM.length bitmap = pure ()
  | otherwise = do
      word <- UM.read bitmap wordIndex
      UM.write bitmap wordIndex (setBit word bitIndex)
  where
    wordIndex =
      key `quot` wordBits
    bitIndex =
      key `rem` wordBits
{-# INLINE setDenseBit #-}

strictFoldM :: (Monad m) => (accumulator -> item -> m accumulator) -> accumulator -> [item] -> m accumulator
strictFoldM step initial items =
  initial `seq` go initial items
  where
    go accumulator remaining =
      case remaining of
        [] ->
          pure accumulator
        item : rest -> do
          next <- step accumulator item
          next `seq` go next rest
{-# INLINE strictFoldM #-}