srtree-3.0.0.0: src/Algorithm/EqSat/Egraph.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-----------------------------------------------------------------------------
-- |
-- Module : Algorithm.EqSat.Egraph
-- Copyright : (c) Fabricio Olivetti 2021 - 2024
-- License : BSD3
-- Maintainer : fabricio.olivetti@gmail.com
-- Stability : experimental
-- Portability :
--
-- Equality Graph data structure
-- Heavily based on hegg (https://github.com/alt-romes/hegg by alt-romes)
--
-----------------------------------------------------------------------------
module Algorithm.EqSat.Egraph where
import Control.Lens (element, makeLenses, view, over, (&), (+~), (-~), (.~), (^.))
--import Control.Monad (forM_, when, foldM, void)
import Data.List ( intercalate, foldl' )
import Control.Monad (forM)
import Control.Monad.State.Strict hiding ( get, put )
import Control.Monad.IO.Class (MonadIO(..))
import Data.Functor.Identity (Identity)
import GHC.Stack (HasCallStack)
import System.Random (StdGen)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (HashSet)
import qualified Data.HashSet as Set
import Data.IntSet (IntSet)
import qualified Data.IntSet as IntSet
import qualified Data.Set as RangeSet
import Data.SRTree
import Data.SRTree.Eval
import Data.SRTree.Recursion (cata)
import Data.Hashable
import Data.Binary
import qualified Data.Binary as Bin
import qualified Data.Vector.Unboxed as VU
import Control.DeepSeq (NFData)
import GHC.Generics
type EClassId = Int -- NOTE: DO NOT CHANGE THIS, this will break the use of IntMap and IntSet
type ClassIdMap = IntMap
-- | N-ary operators represented as flattened multisets inside the e-graph.
-- Only Add and Mul are associative-commutative in this library; the remaining
-- ops (Sub, Div, Power, PowerAbs, AQ) stay binary and live in 'EBin'.
data NOp = EAdd | EMul deriving (Show, Eq, Ord, Enum, Generic, NFData)
-- | The e-graph's node language.
--
-- 'ENAry' stores Add/Mul as a canonical multiset of e-class ids: children are
-- path-compressed, keys sorted by canonical 'EClassId' (commutativity), and
-- nested same-op ENAry children are absorbed at insertion time
-- (associativity), so no commutativity/associativity rewrite rules are needed
-- for Add/Mul. The children are an 'IntMap' of e-class id to multiplicity.
data ENode
= EVar {-# UNPACK #-} !Int
| EParam {-# UNPACK #-} !Int
| EConst {-# UNPACK #-} !Double
| EUni Function EClassId
| EBin Op EClassId EClassId -- Sub | Div | Power | PowerAbs | AQ
| ENAry NOp (IntMap Int) -- canonical multiset: eclass -> multiplicity
deriving (Show, Eq, Generic, NFData)
type EGraphST m a = StateT EGraph m a
type Cost = Int
type CostFun = SRTree Cost -> Cost
type ECache = IntMap.IntMap Target
instance Hashable NOp where
hashWithSalt n EAdd = n `hashWithSalt` (0 :: Int)
hashWithSalt n EMul = n `hashWithSalt` (1 :: Int)
instance Hashable ENode where
hashWithSalt n (EVar ix) = n `hashWithSalt` (0 :: Int) `hashWithSalt` ix
hashWithSalt n (EParam ix) = n `hashWithSalt` (1 :: Int) `hashWithSalt` ix
hashWithSalt n (EConst x) = n `hashWithSalt` (2 :: Int) `hashWithSalt` x
hashWithSalt n (EUni f t) = n `hashWithSalt` (3 :: Int) `hashWithSalt` (fromEnum f) `hashWithSalt` t
hashWithSalt n (EBin op l r) = n `hashWithSalt` (4 :: Int) `hashWithSalt` (fromEnum op) `hashWithSalt` l `hashWithSalt` r
hashWithSalt n (ENAry op m) = n `hashWithSalt` (5 :: Int) `hashWithSalt` op `hashWithSalt` m
type RangeTree a = RangeSet.Set (a, EClassId)
-- | Expand a canonical multiset back to the equivalent (multi-)set of child
-- e-class ids, one entry per occurrence.
expandedList :: IntMap Int -> [EClassId]
expandedList = concatMap (\(k, n) -> replicate n k) . IntMap.toAscList
{-# INLINE expandedList #-}
-- | Build a canonical multiset from a list of child ids (duplicates allowed).
imFromList :: [EClassId] -> IntMap Int
imFromList = IntMap.fromListWith (+) . map (, 1)
{-# INLINE imFromList #-}
insertRange :: (Ord a, Show a) => EClassId -> a -> RangeTree a -> RangeTree a
insertRange eid x = RangeSet.insert (x, eid)
{-# INLINE insertRange #-}
removeRange :: (Ord a, Show a) => EClassId -> a -> RangeTree a -> RangeTree a
removeRange eid x = RangeSet.delete (x, eid)
{-# INLINE removeRange #-}
-- TODO: check this \/
getWithinRange :: Ord a => a -> a -> RangeTree a -> [EClassId]
getWithinRange lb ub rt =
let (_, ge) = RangeSet.split (lb, minBound) rt
(inR, _) = RangeSet.split (ub, maxBound) ge
in map snd (RangeSet.toList inR)
getSmallest :: Ord a => RangeTree a -> Maybe (a, EClassId)
getSmallest = RangeSet.lookupMin
{-# INLINE getSmallest #-}
getGreatest :: Ord a => RangeTree a -> Maybe (a, EClassId)
getGreatest = RangeSet.lookupMax
{-# INLINE getGreatest #-}
-- | Handle to an external, lazily paged e-class store (provided by the
-- storage layer, e.g. srtree-db's 'PageStore'). An 'EGraph' carries one when
-- e-classes are backed by a database; the IO actions fetch / persist /
-- evict a single e-class page. 'Nothing' keeps the classic fully-resident
-- behaviour.
data EClassPageStore = EClassPageStore
{ cpsLookup :: EClassId -> IO (Maybe EClass)
, cpsInsert :: EClass -> IO ()
, cpsDelete :: EClassId -> IO ()
, cpsFlush :: IO () -- ^ write back all pending dirty pages
, cpsAll :: IO [EClass] -- ^ all e-classes currently in the store
, cpsKeys :: IO [EClassId] -- ^ all e-class ids currently in the store
, cpsStreamRoots :: SRTree () -> Int -> [EClassId] -> IO [EClassId] -- ^ bounded candidate roots for an operator, skipping an attempted set
, cpsRecordNode :: ENode -> EClassId -> IO () -- ^ register a newly-created node for write-back
, cpsNodeToClass :: ENode -> IO (Maybe EClassId) -- ^ content-address node -> class lookup (live)
, cpsCanonicalOf :: EClassId -> IO (Maybe EClassId) -- ^ e-class -> canonical representative (live)
, cpsRecordCanonical :: EClassId -> EClassId -> IO () -- ^ persist a canonical mapping (write-back)
, cpsBeginFrontier :: IO () -- ^ start a frontier re-saturation (restrict matcher to changed classes)
, cpsEndFrontier :: IO () -- ^ end it: clear the frontier (a pass re-saturated everything)
}
data EGraph = EGraph { _canonicalMap :: ClassIdMap EClassId -- maps an e-class id to its canonical form
, _eNodeToEClass :: HashMap ENode EClassId -- maps an e-node to its e-class id
, _eClass :: ClassIdMap EClass -- maps an e-class id to its e-class data (resident cache)
, _eDB :: EGraphDB
, _classStore :: Maybe EClassPageStore -- optional lazily paged store for _eClass
}
data EGraphDB = EDB { _worklist :: HashSet (EClassId, ENode) -- e-nodes and e-class schedule for analysis
, _analysis :: HashSet (EClassId, ENode) -- e-nodes and e-class that changed data
, _refits :: IntSet
, _patDB :: DB -- database of patterns
, _fitRangeDB :: RangeTree Double -- database of valid fitness
, _dlRangeDB :: RangeTree Double
, _sizeDB :: IntMap IntSet -- database of model sizes
, _sizeFitDB :: IntMap (RangeTree Double) -- hacky! Size x Fitness DB
, _sizeDLDB :: IntMap (RangeTree Double)
, _unevaluated :: IntSet -- set of not-evaluated e-classes
, _nextId :: Int -- next available id
, _changed :: !Bool -- dirty flag: true if modified since last check
, _trackDBs :: !Bool -- maintain range DBs (False during pure simplify)
, _seenMatches :: Map String (RangeSet.Set String) -- persistent (rule source -> attempted match keys)
} deriving (Show, Generic)
data EClass = EClass { _eClassId :: {-# UNPACK #-} !Int -- e-class id (maybe we don't need that here)
, _eNodes :: HashSet ENode -- set of e-nodes inside this e-class
, _parents :: HashSet (EClassId, ENode) -- parents (e-class, e-node)'s
, _height :: {-# UNPACK #-} !Int -- height
, _info :: EClassData -- data
} deriving (Show, Eq, Generic)
data Consts = NotConst | ParamIx {-# UNPACK #-} !Int | ConstVal {-# UNPACK #-} !Double deriving (Show, Eq, Generic)
data Property = Positive | Negative | NonZero | Real deriving (Show, Eq, Generic) -- TODO: incorporate properties
data EClassData = EData { _cost :: {-# UNPACK #-} !Cost
, _best :: ENode
, _consts :: Consts
, _fitness :: Maybe Double -- NOTE: this cannot be NaN
, _dl :: Maybe Double
, _theta :: [Target]
, _size :: {-# UNPACK #-} !Int
-- , _properties :: Property
-- TODO: include evaluation of expression from this e-class
} deriving (Show, Generic)
-- * Serialization
instance Generic (EClassId, ENode)
instance Binary NOp where
put EAdd = put (0 :: Word8)
put EMul = put (1 :: Word8)
get = do t <- get :: Get Word8
case t of
0 -> pure EAdd
1 -> pure EMul
instance Binary ENode where
put (EVar ix) = put (0 :: Word8) >> put ix
put (EParam ix) = put (1 :: Word8) >> put ix
put (EConst x) = put (2 :: Word8) >> put x
put (EUni f t) = put (3 :: Word8) >> put (fromEnum f) >> put t
put (EBin op l r) = put (4 :: Word8) >> put (fromEnum op) >> put l >> put r
put (ENAry op m) = put (5 :: Word8) >> put op >> put (expandedList m)
get = do t <- get :: Get Word8
case t of
0 -> EVar <$> get
1 -> EParam <$> get
2 -> EConst <$> get
3 -> EUni <$> (toEnum <$> get) <*> get
4 -> EBin <$> (toEnum <$> get) <*> get <*> get
5 -> ENAry <$> get <*> (imFromList <$> get)
instance Binary (SRTree ()) where
put (Var ix) = put (0 :: Word8) >> put ix
put (Param ix) = put (1 :: Word8) >> put ix
put (Const x) = put (2 :: Word8) >> put x
put (Uni f t) = put (3 :: Word8) >> put (fromEnum f)
put (Bin op l r) = put (4 :: Word8) >> put (fromEnum op)
get = do t <- get :: Get Word8
case t of
0 -> Var <$> get
1 -> Param <$> get
2 -> Const <$> get
3 -> Uni <$> (toEnum <$> get) <*> pure ()
4 -> Bin <$> (toEnum <$> get) <*> pure () <*> pure ()
instance (Binary a, Hashable a) => Binary (HashSet a) where
put hs = put (Set.toList hs)
get = Set.fromList <$> get
instance (Binary k, Binary v, Hashable k, Eq k) => Binary (HashMap k v) where
put hm = put (HashMap.toList hm)
get = HashMap.fromList <$> get
instance Binary Target where
put xs = put (VU.toList xs)
get = VU.fromList <$> get
instance Binary IntTrie
instance Binary EClass
instance Binary Consts
instance Binary Property
instance Binary EClassData
-- Custom: keep `_trackDBs` out of the wire format so on-disk EGraphDB data
-- (written before the flag existed) decodes unchanged; it defaults to True.
instance Binary EGraphDB where
put (EDB w a r p f d s sf sdl u n c _ _) =
put w >> put a >> put r >> put p >> put f >> put d >> put s >> put sf >> put sdl >> put u >> put n >> put c
get = EDB <$> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get <*> get <*> pure True <*> pure Map.empty
-- Custom: the wire format omits `_classStore` (a runtime handle to the paged
-- store, never serialized); it decodes to Nothing.
instance Binary EGraph where
put (EGraph c n e d _) = put c >> put n >> put e >> put d
get = EGraph <$> get <*> get <*> get <*> get <*> pure Nothing
instance Eq EClassData where
EData c1 b1 cs1 ft1 dl1 _ s1 == EData c2 b2 cs2 ft2 dl2 _ s2 = c1==c2 && b1==b2 && cs1==cs2 && ft1==ft2 && dl1==dl2 && s1==s2
-- The database maps a symbol to an IntTrie
-- The IntTrie stores the possible paths from a certain e-class
-- that matches a pattern
type DB = Map (SRTree ()) IntTrie
-- The IntTrie is composed of the set of available keys (for convenience)
-- and an IntMap that maps one e-class id to the first child IntTrie,
-- the first child IntTrie will point to the next child and so on
newtype IntTrie = IntTrie { _trie :: IntMap IntTrie } deriving (Generic)
instance Show IntTrie where
show (IntTrie t) = "{" <> intercalate "," (map (\(k,v) -> show k <> " -> " <> show v) $ IntMap.toList t) <> "}"
makeLenses ''EGraph
makeLenses ''EClass
makeLenses ''EClassData
makeLenses ''EGraphDB
-- * Paged e-class access
-- | A monad that can serve e-class data.
--
-- The pure instances ('Identity', 'State StdGen') serve classes from the
-- resident @_eClass@ map; the 'MonadIO' instance consults the optional
-- 'EClassPageStore' when the graph carries one, falling back to the resident
-- map otherwise. All e-class read/write goes through these accessors, which
-- are the single choke point for a paged (out-of-core) e-graph.
class Monad m => ClassStore m where
lookupClass :: EClassId -> EGraphST m (Maybe EClass)
getClass :: HasCallStack => EClassId -> EGraphST m EClass
insertClass :: EClass -> EGraphST m ()
deleteClass :: EClassId -> EGraphST m ()
adjustClass :: EClassId -> (EClass -> EClass) -> EGraphST m ()
-- | Enumerate every e-class (ids / values) in the graph. Paged graphs stream
-- from the store; resident graphs read the full @_eClass@ map.
allClasses :: EGraphST m [EClass]
allKeys :: EGraphST m [EClassId]
-- | Read/write a class directly from/to the backing store, bypassing the
-- resident LRU cache (and its O(n) 'trimResidentCache'). Bulk single-pass
-- traversals such as 'recalculateBestAllStream' must use these: routing every
-- one of ~n classes through 'lookupClass'/'insertClass' inserts each into the
-- resident map and calls 'trimResidentCache' (a full O(n) rebuild) after each
-- write, degenerating to O(n^2) and never terminating at scale.
readDirect :: EClassId -> EGraphST m (Maybe EClass)
writeDirect :: EClass -> EGraphST m ()
allClasses = gets (IntMap.elems . _eClass)
allKeys = gets (IntMap.keys . _eClass)
readDirect = lookupClass
writeDirect = insertClass
-- | Enumerate (bounded) candidate e-class ids that contain a node of the
-- given operator, to drive the streaming matcher, skipping any ids in
-- @exclude@ (the already-attempted seen-set, so the per-rule budget advances
-- to new roots across scheduler cycles). The default reads the resident
-- @_patDB@ trie (the fully-in-RAM path); a paged graph streams the candidates
-- from its backing store instead, so the matcher never builds an O(nodes)
-- structure.
streamRoots :: SRTree () -> Int -> [EClassId] -> EGraphST m [EClassId]
streamRoots = streamRootsFromDB
-- | Record a newly-created e-node (and its e-class) so a streaming matcher's
-- candidate source can see it. The default (fully resident graph) is a no-op:
-- the resident @_patDB@ is already updated by 'addToDB'.
recordNode :: ENode -> EClassId -> EGraphST m ()
recordNode _ _ = pure ()
-- | Content-address node -> class lookup. The default reads the resident
-- @_eNodeToEClass@ map (complete for a resident graph); a paged graph bounds
-- that map and falls back to the backing store on a miss.
lookupNode :: ENode -> EGraphST m (Maybe EClassId)
lookupNode en = gets (HashMap.lookup en . _eNodeToEClass)
-- | Record a node -> class mapping. The default keeps the resident (full)
-- map; a paged graph bounds it (evicting, since the store is authoritative).
insertNode :: ENode -> EClassId -> EGraphST m ()
insertNode en eid = modify' $ over eNodeToEClass (HashMap.insert en eid)
-- | Record a canonical mapping (e-class -> representative), persisting it on a
-- paged graph so the store-backed canonical lookup sees merges/new classes.
insertCanonical :: EClassId -> EClassId -> EGraphST m ()
insertCanonical eid canon = modify' $ over canonicalMap (IntMap.insert eid canon)
-- | The canonical representative of an e-class, or @Nothing@ when unknown. The
-- default reads the resident @_canonicalMap@; a paged graph bounds it and
-- falls back to the store.
canonicalOf :: EClassId -> EGraphST m (Maybe EClassId)
canonicalOf eid = gets (IntMap.lookup eid . _canonicalMap)
-- | Default candidate-root enumeration from the resident @_patDB@ trie, capped
-- at @budget@ after skipping @exclude@ (used by the pure instances and as the
-- no-store fallback for a @MonadIO@ graph).
streamRootsFromDB :: Monad m => SRTree () -> Int -> [EClassId] -> EGraphST m [EClassId]
streamRootsFromDB op budget exclude = do
db <- gets (_patDB . _eDB)
let ex = IntSet.fromList exclude
case Map.lookup op db of
Nothing -> pure []
Just trie -> pure (take budget [ e | e <- IntMap.keys (_trie trie), not (IntSet.member e ex) ])
{-# INLINE streamRootsFromDB #-}
-- | Whether the graph is backed by a lazily paged e-class store. Streaming
-- matchers dispatch on this: a paged graph enumerates candidates from the
-- backing store (bounded memory), a resident graph from @_patDB@.
isPagedGraph :: Monad m => EGraphST m Bool
isPagedGraph = gets (maybe False (const True) . _classStore)
{-# INLINE isPagedGraph #-}
-- Resident-map implementations (used by every pure monad) ------------------
pureLookupClass :: Monad m => EClassId -> EGraphST m (Maybe EClass)
pureLookupClass cid = gets (IntMap.lookup cid . _eClass)
{-# INLINE pureLookupClass #-}
pureGetClass :: (Monad m, HasCallStack) => EClassId -> EGraphST m EClass
pureGetClass cid = do
m <- pureLookupClass cid
case m of
Just ec -> pure ec
Nothing -> error $ "GETECLASS_MISSING eid=" <> show cid
{-# INLINE pureGetClass #-}
pureInsertClass :: Monad m => EClass -> EGraphST m ()
pureInsertClass ec = modify' $ over eClass (IntMap.insert (_eClassId ec) ec)
{-# INLINE pureInsertClass #-}
pureDeleteClass :: Monad m => EClassId -> EGraphST m ()
pureDeleteClass cid = modify' $ over eClass (IntMap.delete cid)
{-# INLINE pureDeleteClass #-}
pureAdjustClass :: Monad m => EClassId -> (EClass -> EClass) -> EGraphST m ()
pureAdjustClass cid f = modify' $ over eClass (IntMap.adjust f cid)
{-# INLINE pureAdjustClass #-}
-- | Maximum number of e-classes kept in the resident @_eClass@ cache when the
-- graph is backed by a paged store. When exceeded, the largest-id classes are
-- retained and the rest evicted from the resident map. The store remains
-- authoritative (and Little-data reads fall back to it), so eviction only
-- bounds memory, never correctness.
residentClassCap :: Int
residentClassCap = 50000
-- | Trim the resident @_eClass@ cache to at most 'residentClassCap' entries
-- by keeping the largest ids. No-op for graphs without a paged store (their
-- resident map must stay complete for the pure instances). Halving on 2x keeps
-- steady churn from triggering an O(n) rebuild on every insert.
trimResidentCache :: Monad m => EGraphST m ()
trimResidentCache = modify' $ \eg ->
case _classStore eg of
Nothing -> eg
Just _ ->
let m = _eClass eg
n = IntMap.size m
in if n <= 2 * residentClassCap
then eg
else over eClass (const (IntMap.fromList (Prelude.drop (n - residentClassCap) (IntMap.toAscList m)))) eg
-- | Bound on the resident @_eNodeToEClass@ cache on a paged graph. Beyond the
-- cap (checked at 2x, halved back to cap) the map is pruned; the backing store
-- is authoritative, so eviction only trades a little dedup accuracy for bounded
-- memory, never correctness.
nodeCacheCap :: Int
nodeCacheCap = 100000
-- | Bound on the resident @_canonicalMap@ cache on a paged graph (same
-- halve-on-2x policy; evicted entries are re-read from the store).
canonicalCacheCap :: Int
canonicalCacheCap = 100000
{-# INLINE nodeCacheCap #-}
{-# INLINE canonicalCacheCap #-}
trimNodeCache :: Monad m => EGraphST m ()
trimNodeCache = modify' $ \eg ->
case _classStore eg of
Nothing -> eg
Just _ ->
let m = _eNodeToEClass eg
n = HashMap.size m
in if n <= 2 * nodeCacheCap
then eg
else over eNodeToEClass (const (HashMap.fromList (Prelude.take nodeCacheCap (HashMap.toList m)))) eg
{-# INLINE trimNodeCache #-}
trimCanonicalCache :: Monad m => EGraphST m ()
trimCanonicalCache = modify' $ \eg ->
case _classStore eg of
Nothing -> eg
Just _ ->
let m = _canonicalMap eg
n = IntMap.size m
in if n <= 2 * canonicalCacheCap
then eg
else over canonicalMap (const (IntMap.fromList (Prelude.take canonicalCacheCap (IntMap.toAscList m)))) eg
{-# INLINE trimCanonicalCache #-}
instance ClassStore Identity where
lookupClass = pureLookupClass
getClass = pureGetClass
insertClass = pureInsertClass
deleteClass = pureDeleteClass
adjustClass = pureAdjustClass
instance ClassStore (State StdGen) where
lookupClass = pureLookupClass
getClass = pureGetClass
insertClass = pureInsertClass
deleteClass = pureDeleteClass
adjustClass = pureAdjustClass
-- Any monad that can run IO is potentially paged: the graph's optional
-- store, when present, is authoritative; otherwise classes come from the
-- resident map.
instance {-# OVERLAPPABLE #-} (Monad m, MonadIO m) => ClassStore m where
-- The resident map is kept in sync by 'insertClass'/'deleteClass', so it is
-- consulted first: repeated reads never touch the store, and a class that
-- was evicted from the store's LRU while still dirty is never served stale.
lookupClass cid = do
eg <- gets id
case IntMap.lookup cid (_eClass eg) of
Just ec -> pure (Just ec)
Nothing -> case _classStore eg of
Nothing -> pure Nothing
Just h -> liftIO (cpsLookup h cid)
getClass cid = do
eg <- gets id
case IntMap.lookup cid (_eClass eg) of
Just ec -> pure ec
Nothing -> case _classStore eg of
Nothing -> pureGetClass cid
Just h -> do
m <- liftIO (cpsLookup h cid)
case m of
Just ec -> do
modify' (over eClass (IntMap.insert cid ec))
trimResidentCache
pure ec
Nothing -> error $ "GETECLASS_MISSING eid=" <> show cid
insertClass ec = do
eg <- gets id
case _classStore eg of
Nothing -> pureInsertClass ec
Just h -> do liftIO (cpsInsert h ec)
pureInsertClass ec
trimResidentCache
deleteClass cid = do
eg <- gets id
case _classStore eg of
Nothing -> pureDeleteClass cid
Just h -> do liftIO (cpsDelete h cid)
pureDeleteClass cid
adjustClass cid f = do
eg <- gets id
case _classStore eg of
Nothing -> pureAdjustClass cid f
Just _ -> do
m <- lookupClass cid
case m of
Nothing -> pure ()
Just ec -> insertClass (f ec)
allClasses = do
eg <- gets id
case _classStore eg of
Nothing -> pure (IntMap.elems (_eClass eg))
Just h -> liftIO (cpsAll h)
allKeys = do
eg <- gets id
case _classStore eg of
Nothing -> pure (IntMap.keys (_eClass eg))
Just h -> liftIO (cpsKeys h)
-- Bypass the resident cache entirely: read the page straight from the store
-- and never insert into the (bounded) resident map, so a bulk traversal over
-- every class stays O(n) instead of O(n^2).
readDirect cid = do
eg <- gets id
case _classStore eg of
Nothing -> pureLookupClass cid
Just h -> liftIO (cpsLookup h cid)
writeDirect ec = do
eg <- gets id
case _classStore eg of
Nothing -> pureInsertClass ec
Just h -> liftIO (cpsInsert h ec)
streamRoots op budget exclude = do
eg <- gets id
case _classStore eg of
Nothing -> streamRootsFromDB op budget exclude
Just h -> liftIO (cpsStreamRoots h op budget exclude)
recordNode en eid = do
eg <- gets id
case _classStore eg of
Nothing -> pure ()
Just h -> liftIO (cpsRecordNode h en eid)
lookupNode en = do
eg <- gets id
case _classStore eg of
Nothing -> gets (HashMap.lookup en . _eNodeToEClass)
Just h -> do
m <- gets (HashMap.lookup en . _eNodeToEClass)
case m of
Just eid -> pure (Just eid)
Nothing -> do
r <- liftIO (cpsNodeToClass h en)
case r of
Just eid -> do insertNode en eid
pure (Just eid)
Nothing -> pure Nothing
insertNode en eid = do
eg <- gets id
case _classStore eg of
Nothing -> modify' $ over eNodeToEClass (HashMap.insert en eid)
Just _ -> do modify' $ over eNodeToEClass (HashMap.insert en eid)
trimNodeCache
insertCanonical eid canon = do
eg <- gets id
case _classStore eg of
Nothing -> modify' $ over canonicalMap (IntMap.insert eid canon)
Just h -> do modify' $ over canonicalMap (IntMap.insert eid canon)
trimCanonicalCache
liftIO (cpsRecordCanonical h eid canon)
canonicalOf eid = do
eg <- gets id
case _classStore eg of
Nothing -> gets (IntMap.lookup eid . _canonicalMap)
Just h -> do
m <- gets (IntMap.lookup eid . _canonicalMap)
case m of
Just c -> pure (Just c)
Nothing -> do
r <- liftIO (cpsCanonicalOf h eid)
case r of
Just c -> do modify' $ over canonicalMap (IntMap.insert eid c)
trimCanonicalCache
pure (Just c)
Nothing -> pure Nothing
-- * E-Graph basic supporting functions
-- | returns an empty e-graph
emptyGraph :: EGraph
emptyGraph = EGraph IntMap.empty HashMap.empty IntMap.empty emptyDB Nothing
{-# INLINE emptyGraph #-}
-- | returns an empty e-graph DB
emptyDB :: EGraphDB
emptyDB = EDB
Set.empty
Set.empty
IntSet.empty
Map.empty
RangeSet.empty
RangeSet.empty
IntMap.empty
IntMap.empty
IntMap.empty
IntSet.empty
0
False
True
Map.empty
{-# INLINE emptyDB #-}
-- | like 'emptyDB' but skips range-DB maintenance (pure simplify mode)
emptyDBNoTrack :: EGraphDB
emptyDBNoTrack = emptyDB{ _trackDBs = False }
{-# INLINE emptyDBNoTrack #-}
-- | an empty e-graph that skips range-DB maintenance (pure simplify mode)
emptyGraphNoTrack :: EGraph
emptyGraphNoTrack = EGraph IntMap.empty HashMap.empty IntMap.empty emptyDBNoTrack Nothing
{-# INLINE emptyGraphNoTrack #-}
-- | Creates a new e-class from an e-class id, a new e-node,
-- and the info of this e-class
createEClass :: EClassId -> ENode -> EClassData -> Int -> EClass
createEClass cId enode' info h = EClass cId (Set.singleton enode') Set.empty h info
{-# INLINE createEClass #-}
-- | gets the canonical id of an e-class with full path compression
canonical :: ClassStore m => EClassId -> EGraphST m EClassId
canonical eclassId = do
mStep <- canonicalOf eclassId
case mStep of
Nothing -> canonError eclassId
Just oneStep
| oneStep == eclassId -> pure eclassId
| otherwise -> do
(root, chain) <- walk [eclassId] oneStep
-- compress the chain in the resident cache (cache-only: the store
-- keeps the authoritative semantic mappings recorded at insert
-- time, so eviction just loses the shortcut, never correctness).
modify' $ \eg -> eg{ _canonicalMap =
foldl' (\m' k -> IntMap.insert k root m') (_canonicalMap eg) chain }
pure root
where
walk :: ClassStore m => [EClassId] -> EClassId -> EGraphST m (EClassId, [EClassId])
walk chain ecId = do
mNext <- canonicalOf ecId
case mNext of
Nothing -> canonError ecId
Just n
| n == ecId -> pure (ecId, chain)
| otherwise -> walk (ecId : chain) n
canonError :: ClassStore m => EClassId -> EGraphST m a
canonError eid = do
m <- gets _canonicalMap
error $ "CANON_MISSING eid=" <> show eid <> " mapSize=" <> show (IntMap.size m)
{-# INLINE canonical #-}
-- | canonize the e-node children
canonize :: (ClassStore m, HasCallStack) => ENode -> EGraphST m ENode
canonize (EVar ix) = pure (EVar ix)
canonize (EParam ix) = pure (EParam ix)
canonize (EConst x) = pure (EConst x)
canonize (EUni f t) = EUni f <$> canonical t
canonize (EBin op l r) = EBin op <$> canonical l <*> canonical r
-- re-map children to their canonical ids; IntMap keeps keys sorted, so
-- commutativity is structural, no rewrite rule required.
canonize (ENAry op m) = do
m' <- IntMap.fromListWith (+) <$> forM (IntMap.toList m) (\(c, n) -> do
c' <- canonical c
pure (c', n))
pure (ENAry op m')
{-# INLINE canonize #-}
-- | The children e-class ids of an e-node.
eChildren :: ENode -> [EClassId]
eChildren (EVar _) = []
eChildren (EParam _) = []
eChildren (EConst _) = []
eChildren (EUni _ t) = [t]
eChildren (EBin _ l r) = [l, r]
eChildren (ENAry _ m) = expandedList m
{-# INLINE eChildren #-}
toOp :: NOp -> Op
toOp EAdd = Add
toOp EMul = Mul
{-# INLINE toOp #-}
-- | Operator shape key used to index the pattern database. ENAry maps back to
-- the corresponding binary operator shape so existing (binary) Add/Mul
-- patterns address the same trie.
eOpKey :: ENode -> SRTree ()
eOpKey (EVar ix) = Var ix
eOpKey (EParam ix) = Param ix
eOpKey (EConst x) = Const x
eOpKey (EUni f _) = Uni f ()
eOpKey (EBin op _ _) = Bin op () ()
eOpKey (ENAry EAdd _) = Bin Add () ()
eOpKey (ENAry EMul _) = Bin Mul () ()
{-# INLINE eOpKey #-}
-- | Convert an e-node (children still as e-class ids) into the equivalent
-- binary SRTree shape. NOTE: only called on non-ENary nodes; flattened
-- ENAry nodes have no binary skeleton (see 'naryTree' / the explicit ENAry
-- cases in the analyses).
fromENode :: ENode -> SRTree EClassId
fromENode (EVar ix) = Var ix
fromENode (EParam ix) = Param ix
fromENode (EConst x) = Const x
fromENode (EUni f t) = Uni f t
fromENode (EBin op l r) = Bin op l r
fromENode (ENAry _ _) = error "fromENode: ENAry has no binary skeleton"
{-# INLINE fromENode #-}
-- | Right-fold a list of e-class child expressions into a binary Fix SRTree
-- for a flattened ENAry multiset (extraction).
naryTree :: NOp -> [Fix SRTree] -> Fix SRTree
naryTree op ts = normalizeSubDiv (foldr1 (\a b -> Fix (Bin (toOp op) a b)) ts)
{-# INLINE naryTree #-}
-- | Re-render the internal negate/recip canonical forms back as Sub/Div so
-- extraction output keeps the familiar shape: `x + (-1)*y` -> `x - y`,
-- `x + (-3)` -> `x - 3` and `x * recip y` -> `x / y`. Sub and Div never
-- appear as e-nodes; they only reappear here during reconstruction.
normalizeSubDiv :: Fix SRTree -> Fix SRTree
normalizeSubDiv = cata alg
where
alg :: SRTree (Fix SRTree) -> Fix SRTree
alg (Bin Add l r) = case pick l r of
Just (pos, neg) -> Fix (Bin Sub pos neg)
Nothing -> Fix (Bin Add l r)
where
pick a b = case negated a of
Just t -> Just (b, t)
Nothing -> case negated b of
Just t -> Just (a, t)
Nothing -> Nothing
negated (Fix (Bin Mul (Fix (Const c)) t)) | c == -1 = Just t
negated (Fix (Bin Mul t (Fix (Const c)))) | c == -1 = Just t
negated (Fix (Const c)) | c < 0 = Just (Fix (Const (-c)))
negated _ = Nothing
alg (Bin Mul l r) = case pick l r of
Just (num, den) -> Fix (Bin Div num den)
Nothing -> Fix (Bin Mul l r)
where
pick a b = case a of
Fix (Uni Recip t) -> Just (b, t)
_ -> case b of
Fix (Uni Recip t) -> Just (a, t)
_ -> Nothing
alg t = Fix t
-- | Convert a binary SRTree (children as e-class ids) into an e-node,
-- flattening Add/Mul into canonical ENAry multisets.
toENode :: (ClassStore m, HasCallStack) => SRTree EClassId -> EGraphST m ENode
toENode (Var ix) = pure (EVar ix)
toENode (Param ix) = pure (EParam ix)
toENode (Const x) = pure (EConst x)
toENode (Uni f t) = EUni f <$> canonical t
toENode (Bin Add l r) = mkENary EAdd [l, r]
toENode (Bin Mul l r) = mkENary EMul [l, r]
toENode (Bin op l r) = EBin op <$> canonical l <*> canonical r
toENode n = error $ "toENode: unsupported node " <> show n
{-# INLINE toENode #-}
-- | Build a canonical ENAry from child ids: canonicalize children, absorb
-- nested same-op ENAry children (associativity), sort by key (commutativity).
mkENary :: (ClassStore m, HasCallStack) => NOp -> [EClassId] -> EGraphST m ENode
mkENary op cids = mkENaryM op (imFromList cids)
-- | Build a canonical ENAry from a canonical multiset of child ids.
mkENaryM :: (ClassStore m, HasCallStack) => NOp -> IntMap Int -> EGraphST m ENode
mkENaryM op m = do
flat <- IntMap.unionsWith (+) <$> mapM (expandM op) (IntMap.toList m)
pure (ENAry op flat)
-- | If the e-class of `cid` holds exactly one e-node and that node is an ENAry
-- of the same op, return its children scaled by `n` (flattening `n`
-- occurrences); otherwise return `n` copies of `cid`. Flattening is only sound
-- through a class with a single node: if the class were merged with other
-- nodes (e.g. `{Add[a,b], Mul[x,c]}`) flattening would silently pick one
-- representative and change the meaning of the term.
expandM :: (ClassStore m, HasCallStack) => NOp -> (EClassId, Int) -> EGraphST m (IntMap Int)
expandM op (cid, n) = do
ec <- getEClass cid
case Set.toList (_eNodes ec) of
[ENAry op' m'] | op' == op -> pure (IntMap.map (* n) m')
_ -> pure (IntMap.singleton cid n)
-- | Reconstruct a binary Fix SRTree from an e-node, right-folding ENAry
-- into nested Bin Add/Mul.
enodeToTree :: (ClassStore m, HasCallStack) => ENode -> EGraphST m (Fix SRTree)
enodeToTree (EVar ix) = pure (Fix (Var ix))
enodeToTree (EParam ix) = pure (Fix (Param ix))
enodeToTree (EConst x) = pure (Fix (Const x))
enodeToTree (EUni f t) = Fix . Uni f <$> getBestExpr t
enodeToTree (EBin op l r) = do
tl <- getBestExpr l
tr <- getBestExpr r
pure (Fix (Bin op tl tr))
enodeToTree (ENAry op m) = do
ts <- mapM getBestExpr (expandedList m)
pure (naryTree op ts)
{-# INLINE enodeToTree #-}
-- | gets an e-class with id `c` (auto-canonizes)
getEClass :: (ClassStore m, HasCallStack) => EClassId -> EGraphST m EClass
getEClass c = do c' <- canonical c; getClass c'
{-# INLINE getEClass #-}
-- | gets the best expression given the default cost function. Cycle-safe and
-- budgeted: see 'getBestExprBounded'.
getBestExpr :: (ClassStore m, HasCallStack) => EClassId -> EGraphST m (Fix SRTree)
getBestExpr eid = getBestExprBounded eid
-- | Like 'getBestExpr' but terminates on pathological graphs: a visited set
-- stops the expansion from re-entering an already-expanded class (a @_best@
-- cycle arising from supersaturation/merges), and a node budget caps the total
-- expanded size (so an exponentially-shared DAG is truncated rather than
-- exploded). Both guards substitute a @Var 0@ placeholder for the part that
-- would otherwise blow up. On well-formed acyclic graphs with small bests
-- neither guard triggers, so the result is identical to the unbounded version.
-- This keeps out-of-core extraction (e.g. 'dbTop') bounded in memory.
getBestExprBounded :: (ClassStore m, HasCallStack) => EClassId -> EGraphST m (Fix SRTree)
getBestExprBounded eid = fst <$> expand Set.empty 0 eid
where
budget :: Int
budget = 200
-- expand returns the tree and the running count of expanded nodes, so the
-- budget bounds the TOTAL size (not just the depth): an exponentially-shared
-- DAG is truncated instead of exploded. A revisited (cyclic) class or a
-- full budget yields a @Var 0@ placeholder.
expand :: ClassStore m => HashSet EClassId -> Int -> EClassId -> EGraphST m (Fix SRTree, Int)
expand _ n _ | n >= budget = pure (Fix (Var 0), n)
expand seen n eid
| Set.member eid seen = pure (Fix (Var 0), n)
| otherwise = do
best <- (_best . _info) <$> getEClass eid
let seen' = Set.insert eid seen
n0 = n + 1
case best of
EVar ix -> pure (Fix (Var ix), n0)
EParam ix -> pure (Fix (Param ix), n0)
EConst x -> pure (Fix (Const x), n0)
EUni f t -> do (tt, n1) <- expand seen' n0 t
pure (Fix (Uni f tt), n1)
EBin op l r -> do
(tl, n1) <- expand seen' n0 l
(tr, n2) <- expand seen' n1 r
pure (Fix (Bin op tl tr), n2)
ENAry op m -> do
(xs, nEnd) <- goNary seen' n0 (IntMap.toAscList m) []
pure (if null xs then (Fix (Var 0), nEnd) else (naryTree op xs, nEnd))
-- build the ENAry children from the multiset WITHOUT materialising the
-- expanded multiplicity list: an enormous count (a pathological supersaturated
-- class) is capped per-child and by the total budget, so each copy counts
-- toward the budget and no giant list is ever allocated.
goNary seen n es acc
| n >= budget = pure (reverse acc, n)
| otherwise = case es of
[] -> pure (reverse acc, n)
((c, cnt) : rest) -> do
(t, n1) <- expand seen n c
let take = min cnt (budget - n1 + 1)
n2 = n1 + (take - 1)
acc' = Prelude.replicate take t ++ acc
goNary seen n2 rest acc'
-- | Creates a singleton trie from an e-class id
trie :: EClassId -> IntMap IntTrie -> IntTrie
trie eid = IntTrie
{-# INLINE trie #-}
-- | Check whether an e-class is a constant value
isConst :: ClassStore m => EClassId -> EGraphST m Bool
isConst eid = do ec <- getEClass eid
case (_consts . _info) ec of
ConstVal _ -> pure True
_ -> pure False
{-# INLINE isConst #-}
getFitness :: ClassStore m => EClassId -> EGraphST m (Maybe Double)
getFitness c = (_fitness . _info) <$> getEClass c
{-# INLINE getFitness #-}
getTheta :: ClassStore m => EClassId -> EGraphST m ([Target])
getTheta c = (_theta . _info) <$> getEClass c
{-# INLINE getTheta #-}
getSize :: ClassStore m => EClassId -> EGraphST m Int
getSize c = (_size . _info) <$> getEClass c
{-# INLINE getSize #-}
isSizeOf :: (Int -> Bool) -> EClass -> Bool
isSizeOf p = p . _size . _info
{-# INLINE isSizeOf #-}
getBestFitness :: ClassStore m => EGraphST m (Maybe Double)
getBestFitness = do
mbec <- gets (fmap snd . getGreatest . _fitRangeDB . _eDB)
case mbec of
Just bec -> (_fitness . _info) <$> getEClass bec
Nothing -> pure Nothing
getDL :: ClassStore m => EClassId -> EGraphST m (Maybe Double)
getDL c = (_dl . _info) <$> getEClass c
{-# INLINE getDL #-}