packages feed

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

-- | The adaptive sparse-push/dense-pull reachability engine and its @runST@
-- seals. Direction is chosen per frontier by work estimates; frozen digraphs
-- traverse the SCC condensation, snapshots traverse the live overlay. The four
-- public entry points are the only openings of 'runST' in the Dense kernel.
module Moonlight.Core.Fixpoint.Dense.Internal.Traverse
  ( frozenReachabilityFrom,
    frozenReachabilityWithPolicy,
    frozenReachabilityWithCache,
    snapshotReachabilityFrom,
  )
where

import Control.Monad.ST (ST, runST)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet (IntSet)
import Data.IntSet qualified as IntSet
import Data.Set qualified as Set
import Data.Vector.Unboxed (Vector)
import Data.Vector.Unboxed qualified as U
import Data.Word (Word32)
import Moonlight.Core.Fixpoint.Dense.Internal.ClosureCache
  ( SccClosureCache,
    closeComponents,
    closureCacheGraph,
  )
import Moonlight.Core.Fixpoint.Dense.Internal.Csr
  ( GraphCsr,
    csrOutDegree,
    csrTargetsForKey,
    csrVertexCount,
    inBounds,
  )
import Moonlight.Core.Fixpoint.Dense.Internal.Policy
  ( ReachabilityPolicy (..),
    defaultReachabilityPolicy,
  )
import Moonlight.Core.Fixpoint.Dense.Internal.Scc
  ( FrozenDigraph (..),
    SccPlan (..),
    expandComponents,
  )
import Moonlight.Core.Fixpoint.Dense.Internal.Scratch
  ( Frontier (..),
    ReachabilityScratch,
    ScratchSide (..),
    appendFreshTarget,
    appendSparseBuffer,
    flipScratchSide,
    frontierContainsAnyM,
    frontierEmpty,
    frontierFoldM',
    frontierMemberM,
    frontierSize,
    isMarked,
    markVectorFresh,
    markVisited,
    newReachabilityScratch,
    nextReachabilityGeneration,
    strictFoldM,
    visitedMarksToIntSet,
    writeDenseFrontierM,
  )
import Moonlight.Core.Fixpoint.Dense.Internal.Snapshot
  ( EdgeTombstones (..),
    GraphSnapshot (..),
    edgeLive,
  )
import Prelude

frozenReachabilityFrom :: FrozenDigraph -> IntSet -> IntSet
frozenReachabilityFrom =
  frozenReachabilityWithPolicy defaultReachabilityPolicy
{-# INLINE frozenReachabilityFrom #-}

frozenReachabilityWithPolicy :: ReachabilityPolicy -> FrozenDigraph -> IntSet -> IntSet
frozenReachabilityWithPolicy policy graph seeds =
  expandComponents plan reachedComponents
  where
    plan = graphSccPlan graph
    seedComponents =
      IntSet.fromList
        [ component
          | v <- IntSet.toAscList (IntSet.filter (inBounds (csrVertexCount (graphForward graph))) seeds),
            Just component <- [sccOfVertex plan U.!? v]
        ]
    reachedComponents =
      adaptiveReachability policy (condensation plan) (condensationBackward plan) seedComponents
{-# INLINE frozenReachabilityWithPolicy #-}

frozenReachabilityWithCache ::
  SccClosureCache ->
  IntSet ->
  (IntSet, SccClosureCache)
frozenReachabilityWithCache cache seeds =
  (expandComponents plan reachedComponents, nextCache)
  where
    plan =
      graphSccPlan graph
    graph =
      closureCacheGraph cache
    seedComponents =
      IntSet.fromList
        [ component
          | v <- IntSet.toAscList (IntSet.filter (inBounds (csrVertexCount (graphForward graph))) seeds),
            Just component <- [sccOfVertex plan U.!? v]
        ]
    (reachedComponents, nextCache) =
      closeComponents seedComponents cache

snapshotReachabilityFrom :: GraphSnapshot -> IntSet -> IntSet
snapshotReachabilityFrom snapshot seeds
  | IntMap.null (insertedEdgeOverlay snapshot) && Set.null tombstones =
      frozenReachabilityFrom (frozenBase snapshot) seeds
  | otherwise =
      snapshotReachabilityWithPolicy defaultReachabilityPolicy snapshot seeds
  where
    tombstones =
      unEdgeTombstones (deletedEdges snapshot)

adaptiveReachability :: ReachabilityPolicy -> GraphCsr -> GraphCsr -> IntSet -> IntSet
adaptiveReachability policy forward backward seeds =
  runST $ do
    scratch <- newReachabilityScratch (csrVertexCount forward)
    adaptiveReachabilityWithScratch policy forward backward scratch seeds
{-# INLINE adaptiveReachability #-}

snapshotReachabilityWithPolicy :: ReachabilityPolicy -> GraphSnapshot -> IntSet -> IntSet
snapshotReachabilityWithPolicy policy snapshot seeds =
  runST $ do
    scratch <- newReachabilityScratch vertexCount
    generation <- nextReachabilityGeneration scratch
    seedLength <- markVectorFresh scratch generation ScratchA boundedSeedVector
    frontier <- frontierFromSparseM policy scratch ScratchA ScratchA seedLength
    let advanceFrontier currentFrontier targetSide = do
          pull <- useSnapshotPullM policy snapshot scratch generation currentFrontier
          if pull
            then snapshotDensePullM snapshot scratch generation currentFrontier targetSide
            else snapshotSparsePushM snapshot scratch generation currentFrontier targetSide
    traverseFrontiersWith policy scratch advanceFrontier frontier ScratchB ScratchB
    visitedMarksToIntSet scratch generation
  where
    vertexCount =
      csrVertexCount (graphForward (frozenBase snapshot))
    boundedSeedVector =
      U.fromList (IntSet.toAscList (IntSet.filter (inBounds vertexCount) seeds))
{-# INLINE snapshotReachabilityWithPolicy #-}

adaptiveReachabilityWithScratch :: ReachabilityPolicy -> GraphCsr -> GraphCsr -> ReachabilityScratch state -> IntSet -> ST state IntSet
adaptiveReachabilityWithScratch policy forward backward scratch seeds = do
  generation <- nextReachabilityGeneration scratch
  seedLength <- markVectorFresh scratch generation ScratchA boundedSeedVector
  frontier <- frontierFromSparseM policy scratch ScratchA ScratchA seedLength
  let advanceFrontier currentFrontier targetSide = do
        pull <- usePullM policy forward backward scratch generation currentFrontier
        if pull
          then densePullM backward scratch generation currentFrontier targetSide
          else sparsePushM forward scratch generation currentFrontier targetSide
  traverseFrontiersWith policy scratch advanceFrontier frontier ScratchB ScratchB
  visitedMarksToIntSet scratch generation
  where
    boundedSeedVector =
      U.fromList (IntSet.toAscList (IntSet.filter (inBounds (csrVertexCount forward)) seeds))
{-# INLINE adaptiveReachabilityWithScratch #-}

traverseFrontiersWith ::
  ReachabilityPolicy ->
  ReachabilityScratch state ->
  (Frontier -> ScratchSide -> ST state Int) ->
  Frontier ->
  ScratchSide ->
  ScratchSide ->
  ST state ()
traverseFrontiersWith policy scratch advanceFrontier frontier nextSparseSide nextDenseSide
  | frontierEmpty frontier = pure ()
  | otherwise = do
      nextLength <- advanceFrontier frontier nextSparseSide
      nextFrontier <- frontierFromSparseM policy scratch nextSparseSide nextDenseSide nextLength
      traverseFrontiersWith
        policy
        scratch
        advanceFrontier
        nextFrontier
        (flipScratchSide nextSparseSide)
        (flipScratchSide nextDenseSide)
{-# INLINE traverseFrontiersWith #-}

usePullM :: ReachabilityPolicy -> GraphCsr -> GraphCsr -> ReachabilityScratch state -> Word32 -> Frontier -> ST state Bool
usePullM policy forward backward scratch generation frontier
  | frontierSize frontier <= smallFrontierLimit policy = pure False
  | otherwise = do
      pullWork <- max 1 <$> unvisitedIncomingWorkM backward scratch generation
      pushWork <- frontierWorkM forward scratch frontier
      pure (fromIntegral pushWork > frontierPullThreshold policy frontier * fromIntegral pullWork)
{-# INLINE usePullM #-}

useSnapshotPullM :: ReachabilityPolicy -> GraphSnapshot -> ReachabilityScratch state -> Word32 -> Frontier -> ST state Bool
useSnapshotPullM policy snapshot =
  usePullM policy (graphForward (frozenBase snapshot)) (graphBackward (frozenBase snapshot))
{-# INLINE useSnapshotPullM #-}

frontierPullThreshold :: ReachabilityPolicy -> Frontier -> Double
frontierPullThreshold policy frontier =
  case frontier of
    SparseFrontier _ _ ->
      pushToPullRatio policy
    DenseFrontier _ _ ->
      pullToPushRatio policy
{-# INLINE frontierPullThreshold #-}

frontierFromSparseM :: ReachabilityPolicy -> ReachabilityScratch state -> ScratchSide -> ScratchSide -> Int -> ST state Frontier
frontierFromSparseM policy scratch sparseSide denseSide frontierLength
  | frontierLength <= smallFrontierLimit policy = pure (SparseFrontier sparseSide frontierLength)
  | otherwise = do
      writeDenseFrontierM scratch sparseSide denseSide frontierLength
      pure (DenseFrontier denseSide frontierLength)
{-# INLINE frontierFromSparseM #-}

frontierWorkM :: GraphCsr -> ReachabilityScratch state -> Frontier -> ST state Int
frontierWorkM csr scratch =
  frontierFoldM' scratch (\acc v -> pure (acc + csrOutDegree csr v)) 0
{-# INLINE frontierWorkM #-}

unvisitedIncomingWorkM :: GraphCsr -> ReachabilityScratch state -> Word32 -> ST state Int
unvisitedIncomingWorkM backward scratch generation =
  strictFoldM step 0 [0 .. csrVertexCount backward - 1]
  where
    step acc vertex = do
      seen <- isMarked scratch generation vertex
      pure (if seen then acc else acc + csrOutDegree backward vertex)
{-# INLINE unvisitedIncomingWorkM #-}

sparsePushM :: GraphCsr -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> ST state Int
sparsePushM forward scratch generation frontier targetSide =
  frontierFoldM'
    scratch
    ( \fresh source ->
        U.foldM'
          (appendFreshTarget scratch generation targetSide)
          fresh
          (csrTargetsForKey forward source)
    )
    0
    frontier
{-# INLINE sparsePushM #-}

snapshotSparsePushM :: GraphSnapshot -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> ST state Int
snapshotSparsePushM snapshot scratch generation frontier targetSide =
  frontierFoldM'
    scratch
    (appendFreshSnapshotTargets snapshot scratch generation targetSide)
    0
    frontier
{-# INLINE snapshotSparsePushM #-}

snapshotDensePullM :: GraphSnapshot -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> ST state Int
snapshotDensePullM snapshot scratch generation frontier targetSide = do
  baseFreshLength <- snapshotDenseBasePullM snapshot scratch generation frontier targetSide
  snapshotOverlayPushM snapshot scratch generation frontier targetSide baseFreshLength
{-# INLINE snapshotDensePullM #-}

snapshotDenseBasePullM :: GraphSnapshot -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> ST state Int
snapshotDenseBasePullM snapshot scratch generation frontier targetSide =
  strictFoldM step 0 [0 .. csrVertexCount backward - 1]
  where
    backward =
      graphBackward (frozenBase snapshot)

    step freshLength target = do
      seen <- isMarked scratch generation target
      if seen
        then pure freshLength
        else do
          reached <- frontierContainsAnyLiveBaseIncomingM snapshot scratch frontier target (csrTargetsForKey backward target)
          if reached
            then markVisited scratch generation target *> appendSparseBuffer scratch targetSide freshLength target
            else pure freshLength
{-# INLINE snapshotDenseBasePullM #-}

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

snapshotOverlayPushM :: GraphSnapshot -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> Int -> ST state Int
snapshotOverlayPushM snapshot scratch generation frontier targetSide initialFreshLength =
  frontierFoldM'
    scratch
    (appendFreshSnapshotOverlayTargets snapshot scratch generation targetSide)
    initialFreshLength
    frontier
{-# INLINE snapshotOverlayPushM #-}

appendFreshSnapshotOverlayTargets ::
  GraphSnapshot ->
  ReachabilityScratch state ->
  Word32 ->
  ScratchSide ->
  Int ->
  Int ->
  ST state Int
appendFreshSnapshotOverlayTargets snapshot scratch generation targetSide freshLength source =
  strictFoldM
    (appendLiveSnapshotTarget snapshot scratch generation targetSide source)
    freshLength
    (IntSet.toAscList (IntMap.findWithDefault IntSet.empty source (insertedEdgeOverlay snapshot)))
{-# INLINE appendFreshSnapshotOverlayTargets #-}

appendFreshSnapshotTargets ::
  GraphSnapshot ->
  ReachabilityScratch state ->
  Word32 ->
  ScratchSide ->
  Int ->
  Int ->
  ST state Int
appendFreshSnapshotTargets snapshot scratch generation targetSide freshLength source = do
  baseFreshLength <-
    U.foldM'
      (appendLiveSnapshotTarget snapshot scratch generation targetSide source)
      freshLength
      (csrTargetsForKey (graphForward (frozenBase snapshot)) source)
  strictFoldM
    (appendLiveSnapshotTarget snapshot scratch generation targetSide source)
    baseFreshLength
    (IntSet.toAscList (IntMap.findWithDefault IntSet.empty source (insertedEdgeOverlay snapshot)))
{-# INLINE appendFreshSnapshotTargets #-}

appendLiveSnapshotTarget ::
  GraphSnapshot ->
  ReachabilityScratch state ->
  Word32 ->
  ScratchSide ->
  Int ->
  Int ->
  Int ->
  ST state Int
appendLiveSnapshotTarget snapshot scratch generation targetSide source freshLength target
  | edgeLive snapshot source target =
      appendFreshTarget scratch generation targetSide freshLength target
  | otherwise =
      pure freshLength
{-# INLINE appendLiveSnapshotTarget #-}

densePullM :: GraphCsr -> ReachabilityScratch state -> Word32 -> Frontier -> ScratchSide -> ST state Int
densePullM backward scratch generation frontier targetSide =
  strictFoldM step 0 [0 .. csrVertexCount backward - 1]
  where
    step freshLength vertex = do
      seen <- isMarked scratch generation vertex
      if seen
        then pure freshLength
        else do
          reached <- frontierContainsAnyM scratch frontier (csrTargetsForKey backward vertex)
          if reached
            then markVisited scratch generation vertex *> appendSparseBuffer scratch targetSide freshLength vertex
            else pure freshLength
{-# INLINE densePullM #-}