packages feed

moonlight-core-0.1.0.3: src-solver/Moonlight/Core/UnionFind/Transaction.hs

-- | A sealed mutable execution mode for the canonical persistent union-find.
-- Dense non-negative prefixes use mutable parent/rank arrays with in-place path
-- compression; sparse, negative, and giant keys stay in an 'IntMap' overlay.
-- Successful transactions freeze back to the immutable 'UnionFind' owner.
--
-- This module is the public face: the two transaction runners (the only
-- @runST@ seals) and the query/mutation verbs. The mutable arena lives in
-- sealed @Transaction.Internal.*@ owners; 'UnionFindEditor' is re-exported
-- abstractly so no external caller can forge or dissect editor state.
module Moonlight.Core.UnionFind.Transaction
  ( UnionFindEditor,
    UnionOutcome (..),
    UnionFindAllocationError (..),
    runUnionFindTransaction,
    runUnionFindTransactionEither,
    transactionMember,
    transactionFind,
    transactionFindExisting,
    transactionCanonicalClass,
    transactionInsertClassId,
    transactionMakeSet,
    transactionUnion,
    transactionEquivalent,
    transactionCanonicalMapAndCompress,
    transactionCompress,
  )
where

import Control.Monad.ST (ST, runST)
import Data.Foldable (traverse_)
import Data.IntMap.Strict (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.Maybe (isJust)
import Data.STRef (modifySTRef', readSTRef, writeSTRef)
import Moonlight.Core.Identifier.EGraph (ClassId (..), classIdKey)
import Moonlight.Core.UnionFind.Internal.Semantics (chooseLink, rootKeyBy)
import Moonlight.Core.UnionFind.Internal.Types
  ( UnionFind,
    UnionFindAllocationError (..),
    advanceNextFreshForClassIdKey,
    allocateNextClassId,
  )
import Moonlight.Core.UnionFind.Transaction.Internal.Algorithm
  ( applyLinkDecision,
    compressRootKey,
  )
import Moonlight.Core.UnionFind.Transaction.Internal.Lifecycle
  ( freezeUnionFind,
    thawUnionFind,
  )
import Moonlight.Core.UnionFind.Transaction.Internal.Snapshot
  ( parentMap,
  )
import Moonlight.Core.UnionFind.Transaction.Internal.Storage
  ( readParentKey,
    readRankValue,
    writeFreshClassIdentity,
  )
import Moonlight.Core.UnionFind.Transaction.Internal.Types
  ( UnionFindEditor (nextFresh),
    UnionOutcome (..),
  )
import Prelude

runUnionFindTransaction ::
  UnionFind ->
  (forall state. UnionFindEditor state -> ST state value) ->
  (value, UnionFind)
runUnionFindTransaction base action =
  runST $ do
    editor <- thawUnionFind base
    value <- action editor
    committed <- freezeUnionFind editor
    pure (value, committed)

runUnionFindTransactionEither ::
  UnionFind ->
  (forall state. UnionFindEditor state -> ST state (Either err value)) ->
  Either err (value, UnionFind)
runUnionFindTransactionEither base action =
  runST $ do
    editor <- thawUnionFind base
    outcome <- action editor
    case outcome of
      Left err ->
        pure (Left err)
      Right value -> do
        committed <- freezeUnionFind editor
        pure (Right (value, committed))

transactionMember :: UnionFindEditor state -> ClassId -> ST state Bool
transactionMember editor classId =
  fmap isJust (readParentKey editor (classIdKey classId))
{-# INLINE transactionMember #-}

transactionFind :: UnionFindEditor state -> ClassId -> ST state ClassId
transactionFind editor classId =
  fmap ClassId (compressRootKey editor (classIdKey classId))
{-# INLINE transactionFind #-}

transactionFindExisting ::
  UnionFindEditor state ->
  ClassId ->
  ST state (Maybe ClassId)
transactionFindExisting editor classId = do
  exists <- transactionMember editor classId
  if exists
    then fmap Just (transactionFind editor classId)
    else pure Nothing
{-# INLINE transactionFindExisting #-}

transactionCanonicalClass ::
  UnionFindEditor state ->
  ClassId ->
  ST state (Maybe ClassId)
transactionCanonicalClass editor classId = do
  exists <- transactionMember editor classId
  if exists
    then fmap (Just . ClassId) (rootKeyBy (readParentKey editor) (classIdKey classId))
    else pure Nothing
{-# INLINE transactionCanonicalClass #-}

transactionInsertClassId :: UnionFindEditor state -> ClassId -> ST state ()
transactionInsertClassId editor classId = do
  let key = classIdKey classId
  modifySTRef' (nextFresh editor) (advanceNextFreshForClassIdKey key)
  exists <- fmap isJust (readParentKey editor key)
  if exists
    then pure ()
    else writeFreshClassIdentity editor key
{-# INLINE transactionInsertClassId #-}

transactionMakeSet :: UnionFindEditor state -> ST state (Either UnionFindAllocationError ClassId)
transactionMakeSet editor = do
  currentNextFresh <- readSTRef (nextFresh editor)
  case allocateNextClassId currentNextFresh of
    Left allocationError ->
      pure (Left allocationError)
    Right (classId, updatedNextFresh) -> do
      writeFreshClassIdentity editor (classIdKey classId)
      writeSTRef (nextFresh editor) updatedNextFresh
      pure (Right classId)
{-# INLINE transactionMakeSet #-}

transactionUnion ::
  UnionFindEditor state ->
  ClassId ->
  ClassId ->
  ST state UnionOutcome
transactionUnion editor leftClassId rightClassId = do
  transactionInsertClassId editor leftClassId
  transactionInsertClassId editor rightClassId
  leftRoot <- transactionFind editor leftClassId
  rightRoot <- transactionFind editor rightClassId
  if leftRoot == rightRoot
    then pure (AlreadyEquivalent leftRoot)
    else do
      leftRank <- readRankValue editor (classIdKey leftRoot)
      rightRank <- readRankValue editor (classIdKey rightRoot)
      applyLinkDecision editor (chooseLink leftRoot leftRank rightRoot rightRank)
{-# INLINE transactionUnion #-}

transactionEquivalent ::
  UnionFindEditor state ->
  ClassId ->
  ClassId ->
  ST state Bool
transactionEquivalent editor leftClassId rightClassId = do
  leftRoot <- rootKeyBy (readParentKey editor) (classIdKey leftClassId)
  rightRoot <- rootKeyBy (readParentKey editor) (classIdKey rightClassId)
  pure (leftRoot == rightRoot)
{-# INLINE transactionEquivalent #-}

transactionCanonicalMapAndCompress ::
  UnionFindEditor state ->
  ST state (IntMap ClassId)
transactionCanonicalMapAndCompress editor = do
  currentParents <- parentMap editor
  IntMap.traverseWithKey
    (\key _ -> transactionFind editor (ClassId key))
    currentParents

-- | Compress every parent chain in place; the freeze commits the flat
-- structure.
transactionCompress :: UnionFindEditor state -> ST state ()
transactionCompress editor = do
  currentParents <- parentMap editor
  traverse_ (compressRootKey editor) (IntMap.keys currentParents)
{-# INLINE transactionCompress #-}