srtree-3.0.0.0: src/Algorithm/EqSat/Store.hs
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Algorithm.EqSat.Store
( GraphRows(..)
, EClassRow(..)
, exportEGraph
, importEGraph
, mergeEGraph
, rebuildDBs
) where
import Control.Lens ( over )
import Control.Monad ( forM, forM_, foldM )
import Control.Monad.Identity ( Identity, runIdentity )
import Control.Monad.State.Strict ( StateT, execStateT, modify', gets )
import GHC.Generics ( Generic )
import GHC.Stack ( HasCallStack )
import qualified Data.HashMap.Strict as HashMap
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashSet as Set
import qualified Data.IntMap.Strict as IntMap
import Data.IntMap.Strict ( IntMap )
import qualified Data.IntSet as IntSet
import qualified Data.Set as RangeSet
import Data.List ( sortOn )
import Data.SRTree
import Algorithm.EqSat.Egraph
import Algorithm.EqSat.Build
-- | Row representation of the core (structural) state of an e-graph,
-- normalized for external storage (e.g. a relational DB).
data GraphRows = GraphRows
{ _grCanonical :: IntMap EClassId -- ^ eid -> canonical representative (self-loop for roots)
, _grENodeToEClass :: HashMap ENode EClassId -- ^ canonical e-node -> its e-class
, _grEClasses :: IntMap EClassRow -- ^ canonical e-class id -> data row
, _grNextId :: Int -- ^ next free e-class id
, _grTrackDBs :: Bool -- ^ whether range DBs are maintained
} deriving (Show, Eq, Generic)
-- | Per-e-class data row.
data EClassRow = EClassRow
{ _rcNodes :: Set.HashSet ENode
, _rcParents :: Set.HashSet (EClassId, ENode)
, _rcHeight :: Int
, _rcInfo :: EClassData
} deriving (Show, Eq, Generic)
-- | Export the core structural state of an e-graph into a normalised row format.
exportEGraph :: EGraph -> GraphRows
exportEGraph eg = GraphRows
{ _grCanonical = _canonicalMap eg
, _grENodeToEClass = _eNodeToEClass eg
, _grEClasses = IntMap.map toRow (_eClass eg)
, _grNextId = _nextId (_eDB eg)
, _grTrackDBs = _trackDBs (_eDB eg)
}
where
toRow ec = EClassRow (_eNodes ec) (_parents ec) (_height ec) (_info ec)
-- | Reconstruct an e-graph from normalised rows, rebuilding all derived indexes.
--
-- Real e-graphs may carry stale @_eNodeToEClass@ entries left behind by
-- merges (a node pointing at a class whose canonical representative is
-- another class). Such entries are canonicalized at import: node -> class
-- values are routed through the canonical map and any non-root class rows
-- are dropped. Parent pointers are recomputed from the canonicalized node
-- map so they never reference dead classes.
importEGraph :: GraphRows -> Either String EGraph
importEGraph rows
| not (validate rows) = Left (validationMsg rows)
| otherwise = Right (runIdentity $ execStateT rebuildDBs (buildCore (canonicalize rows)))
-- | Normalize stale rows: route node->class values through the canonical map
-- and drop non-root class rows.
--
-- Parent pointers come from the stored @_rcParents@ when a class has any
-- (e.g. after a storage-layer round-trip through the @parent@ table); parent
-- class ids are routed through the canonical map so they never reference dead
-- classes. Classes without stored parents (legacy rows, hand-built rows) fall
-- back to recomputing parents from the canonicalized node map.
canonicalize :: GraphRows -> GraphRows
canonicalize rows =
let canon = _grCanonical rows
rep eid = IntMap.findWithDefault eid eid canon
nodeMap' = HashMap.map rep (_grENodeToEClass rows)
classes' = IntMap.filterWithKey
(\eid _ -> IntMap.lookup eid canon == Just eid)
(_grEClasses rows)
parents' = IntMap.fromListWith Set.union
[ (c, Set.singleton (eid, en))
| (en, eid) <- HashMap.toList nodeMap'
, c <- eChildren en ]
stored' = IntMap.mapWithKey
(\_ r -> Set.map (\(pEid, pEn) -> (rep pEid, pEn)) (_rcParents r))
classes'
fixRow eid r =
let stored = IntMap.findWithDefault Set.empty eid stored'
in r { _rcParents = if Set.null stored
then IntMap.findWithDefault Set.empty eid parents'
else stored }
in rows { _grENodeToEClass = nodeMap'
, _grEClasses = IntMap.mapWithKey fixRow classes' }
buildCore :: GraphRows -> EGraph
buildCore rows = EGraph
{ _canonicalMap = _grCanonical rows
, _eNodeToEClass = _grENodeToEClass rows
, _eClass = IntMap.mapWithKey mkEClass (_grEClasses rows)
, _eDB = (emptyDB){ _nextId = _grNextId rows, _trackDBs = _grTrackDBs rows }
, _classStore = Nothing
}
where
mkEClass eid r = EClass eid (_rcNodes r) (_rcParents r) (_rcHeight r) (_rcInfo r)
rebuildDBs :: EGraphST Identity ()
rebuildDBs = do
-- Rebuild the pattern database from the canonical e-node -> class mapping
nodes <- gets _eNodeToEClass
forM_ (HashMap.toList nodes) $ \(en, eid) -> addToDB en eid
-- Rebuild range/size indexes from class info
classes <- gets _eClass
forM_ (IntMap.toList classes) $ \(eid, ec) -> do
let info = _info ec
sz = _size info
fit = _fitness info
dl = _dl info
modify' $ over (eDB . sizeDB) (IntMap.insertWith IntSet.union sz (IntSet.singleton eid))
case fit of
Nothing -> modify' $ over (eDB . unevaluated) (IntSet.insert eid)
Just fn -> modify' $ over (eDB . fitRangeDB) (insertRange eid fn)
. over (eDB . sizeFitDB) (IntMap.insertWith RangeSet.union sz (RangeSet.singleton (fn, eid)))
case dl of
Nothing -> pure ()
Just dn -> modify' $ over (eDB . dlRangeDB) (insertRange eid dn)
. over (eDB . sizeDLDB) (IntMap.insertWith RangeSet.union sz (RangeSet.singleton (dn, eid)))
-- | Validate that the exported rows form a consistent graph.
--
-- All referenced ids must be present in the canonical map. Node -> class
-- values and class rows may reference classes that are not their own
-- canonical representative (stale entries left behind by merges); those are
-- repaired by 'canonicalize' during import.
validate :: GraphRows -> Bool
validate rows =
let canon = _grCanonical rows
classes = _grEClasses rows
nodeIds = HashMap.keys (_grENodeToEClass rows)
extraIds = IntMap.keys classes
++ HashMap.elems (_grENodeToEClass rows)
++ concatMap eChildren nodeIds
inCanon = all (`IntMap.member` canon) extraIds
nextOk = _grNextId rows >= 0
in inCanon && nextOk
validationMsg :: GraphRows -> String
validationMsg rows
| not inCanon = "some e-node/e-class id is not present in the canonical map"
| not nextOk = "next id is negative"
| otherwise = "invalid GraphRows"
where
canon = _grCanonical rows
classes = _grEClasses rows
nodeIds = HashMap.keys (_grENodeToEClass rows)
extraIds = IntMap.keys classes
++ HashMap.elems (_grENodeToEClass rows)
++ concatMap eChildren nodeIds
inCanon = all (`IntMap.member` canon) extraIds
nextOk = _grNextId rows >= 0
-- | Return canonical e-class ids ordered children-before-parents (ascending height).
classOrder :: GraphRows -> Either String [EClassId]
classOrder rows =
Right $ map fst $ sortOn (_rcHeight . snd) $ IntMap.toAscList (_grEClasses rows)
-- | Remap a B-e-graph's e-node into A's id-space using the correspondence map.
remapNode
:: GraphRows -- ^ rows of graph B (source)
-> IntMap EClassId -- ^ corr: B canonical id -> A id
-> ENode
-> Either String ENode
remapNode rowsB corr = go
where
canonB :: EClassId -> EClassId
canonB cid = IntMap.findWithDefault cid cid (_grCanonical rowsB)
toA :: EClassId -> Either String EClassId
toA cid =
case IntMap.lookup (canonB cid) corr of
Just eidA -> Right eidA
Nothing -> Left ("child " <> show cid <> " of graph B not yet merged")
go (EVar ix) = Right (EVar ix)
go (EParam ix) = Right (EParam ix)
go (EConst x) = Right (EConst x)
go (EUni f t) = EUni f <$> toA t
go (EBin op l r) = EBin op <$> toA l <*> toA r
go (ENAry op m) = do
m' <- foldM step IntMap.empty (IntMap.toList m)
Right (ENAry op m')
where
step acc (cid, n) = do
cidA <- toA cid
pure (IntMap.insertWith (+) cidA n acc)
-- | Merge class ids by unioning their e-classes under the given cost function.
mergeClass :: HasCallStack => CostFun -> EClassId -> EClassId -> EGraphST Identity EClassId
mergeClass costFun x y =
if x == y then pure x else merge costFun x y
-- | Structurally merge graph @b@ into a copy of graph @a@.
--
-- The e-nodes of @b@ are canonicalized under @a@'s id space, deduplicated
-- against @a@'s existing content, and equivalent classes are unioned. Cost and
-- best of newly introduced content are computed with @costFun@ (i.e. merging
-- adopts @a@'s cost function). Dataset-specific values (fitness/DL/theta) are
-- NOT transferred: they are per-dataset data managed by the storage layer.
mergeEGraph :: HasCallStack => CostFun -> EGraph -> EGraph -> Either String EGraph
mergeEGraph costFun a b =
let rowsB = exportEGraph b
in case classOrder rowsB of
Left err -> Left err
Right order -> Right (runIdentity $ execStateT (step IntMap.empty order) a)
where
step :: IntMap EClassId -> [EClassId] -> EGraphST Identity ()
step _ [] = rebuild costFun
step corr (bCanon : rest) = do
let ec = _grEClasses rowsB IntMap.! bCanon
resolved <- forM (Set.toList (_rcNodes ec)) $ \en ->
case remapNode rowsB corr en of
Left err -> pure (Left err)
Right enA -> Right <$> add costFun enA
case sequence resolved of
Left err -> error ("mergeEGraph: " <> err) -- pre-validated
Right [] -> step corr rest
Right (x : xs) -> do
rep <- foldM (mergeClass costFun) x xs
step (IntMap.insert bCanon rep corr) rest
rowsB = exportEGraph b