overeasy-0.1.1: test/Main.hs
module Main (main) where
import Control.DeepSeq (NFData, force)
import Control.Exception (evaluate)
import Control.Monad (foldM, unless, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.State.Strict (MonadState (..), State, StateT, evalState, evalStateT, execState, execStateT, gets,
runState)
import Control.Monad.Trans (MonadTrans (..))
import Data.Bifunctor (bimap)
import Data.Char (chr, ord)
import Data.Coerce (coerce)
import Data.Foldable (for_)
import Data.Hashable (Hashable)
import qualified Data.HashMap.Strict as HashMap
import Data.List (delete)
import Data.Maybe (fromJust, isJust)
import Data.Semigroup (Max (..))
import qualified Data.Sequence as Seq
import Data.Traversable (for)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import qualified IntLike.Equiv as ILE
import qualified IntLike.Graph as ILG
import IntLike.Map (IntLikeMap)
import qualified IntLike.Map as ILM
import IntLike.Set (IntLikeSet)
import qualified IntLike.Set as ILS
import Overeasy.Assoc (Assoc, AssocInsertRes (..), assocBwd, assocCanCompact, assocCompact, assocEquiv, assocFromList,
assocFwd, assocInsert, assocLeaves, assocMember, assocMembers, assocNew, assocPartialLookupByKey,
assocRoots, assocSize)
import Overeasy.EGraph (EAnalysis, EClassId (..), EClassInfo (..), EGraph (..), ENodeId (..), MergeResult (..),
egAddTerm, egCanonicalize, egClassSize, egFindTerm, egMerge, egMergeMany, egNew, egNodeSize,
noAnalysis)
import Overeasy.EquivFind (EquivFind (..), efAdd, efCanCompact, efCompact, efFindRoot, efLeaves, efLeavesSize, efMember,
efMembers, efMerge, efMergeSets, efNew, efRemoveAll, efRoots, efRootsSize, efTotalSize)
import Overeasy.Example (Arith (..), ArithF (..))
import Overeasy.Matching (Match (..), MatchPat (..), MatchSubst (..), Pat, match)
import Overeasy.Util (Changed (..))
import PropUnit (DependencyType (..), Gen, MonadTest, PropertyT, Range, TestLimit, TestTree, after, assert, forAll,
testGroup, testMain, testProp, testUnit, (/==), (===))
import Test.Overeasy.BinTree (BinTree, pattern BinTreeBranch, BinTreeF (..), pattern BinTreeLeaf)
import Unfree (pattern FreeEmbed, pattern FreePure)
fullyEvaluate :: (MonadIO m, NFData a) => a -> m a
fullyEvaluate = liftIO . evaluate . force
applyS :: Monad m => State s a -> StateT s m a
applyS = state . runState
testS :: Monad m => (s -> m a) -> StateT s m a
testS p = get >>= lift . p
applyTestS :: Monad m => State s a -> (a -> s -> m b) -> StateT s m b
applyTestS act check = do
a <- applyS act
s <- get
lift (check a s)
foldS_ :: (Monad m, Foldable t) => s -> t a -> (a -> StateT s m ()) -> m s
foldS_ z as f = execStateT (for_ as f) z
runS :: Monad m => s -> StateT s m () -> m ()
runS = flip evalStateT
flipFoldM :: Monad m => b -> [a] -> (b -> a -> m b) -> m b
flipFoldM b as f = foldM f b as
newtype V = V { unV :: Int }
deriving newtype (Eq, Ord, Hashable, NFData)
instance Show V where
show = show . fromV
toV :: Char -> V
toV = V . ord
fromV :: V -> Char
fromV = chr . unV
setV :: String -> IntLikeSet V
setV = ILS.fromList . fmap toV
mapV :: [(Char, Char)] -> IntLikeMap V V
mapV = ILM.fromList . fmap (bimap toV toV)
multiMapV :: [(Char, String)] -> IntLikeMap V (IntLikeSet V)
multiMapV = ILM.fromList . fmap (bimap toV setV)
type EF = EquivFind V
testEfSimple :: TestTree
testEfSimple = testUnit "EF simple" $ runS efNew $ do
testS $ \ef -> do
efRootsSize ef === 0
efLeavesSize ef === 0
efTotalSize ef === 0
efRoots ef === []
efLeaves ef === []
efMembers ef === []
efMember (toV 'a') ef === False
efMember (toV 'c') ef === False
efFwd ef === ILM.empty
efBwd ef === ILM.empty
_ <- applyS (efAdd (toV 'a'))
testS $ \ef -> do
efRootsSize ef === 1
efLeavesSize ef === 0
efTotalSize ef === 1
ILS.fromList (efRoots ef) === setV "a"
ILS.fromList (efLeaves ef) === ILS.empty
efFwd ef === multiMapV [('a', "")]
efBwd ef === ILM.empty
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
testS $ \ef -> do
efRootsSize ef === 3
efLeavesSize ef === 0
efTotalSize ef === 3
ILS.fromList (efRoots ef) === setV "abc"
ILS.fromList (efLeaves ef) === ILS.empty
efFwd ef === multiMapV [('a', ""), ('b', ""), ('c', "")]
efBwd ef === ILM.empty
applyTestS (efMerge (toV 'a') (toV 'c')) $ \res ef -> do
res === Just (toV 'a', setV "c")
efRootsSize ef === 2
efLeavesSize ef === 1
efTotalSize ef === 3
ILS.fromList (efRoots ef) === setV "ab"
ILS.fromList (efLeaves ef) === setV "c"
efFwd ef === multiMapV [('a', "c"), ('b', "")]
efBwd ef === mapV [('c', 'a')]
efMembers ef === fmap toV ['a', 'b', 'c']
efMember (toV 'a') ef === True
efMember (toV 'c') ef === True
applyTestS (efMerge (toV 'c') (toV 'a')) $ \res _ -> res === Nothing
applyTestS (efMerge (toV 'b') (toV 'z')) $ \res _ -> res === Nothing
resetEf :: StateT EF (PropertyT IO) ()
resetEf = do
put efNew
_ <- applyS (efAdd (toV 'a'))
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
_ <- applyS (efMerge (toV 'a') (toV 'c'))
ef <- get
efFwd ef === multiMapV [('a', "c"), ('b', "")]
efBwd ef === mapV [('c', 'a')]
addExtraEf :: StateT EF (PropertyT IO) ()
addExtraEf = do
_ <- applyS (efAdd (toV 'd'))
_ <- applyS (efMerge (toV 'a') (toV 'd'))
ef <- get
efFwd ef === multiMapV [('a', "cd"), ('b', "")]
efBwd ef === mapV [('c', 'a'), ('d', 'a')]
testEfRemove :: TestTree
testEfRemove = testUnit "EF remove" $ runS efNew $ do
-- remove leav
resetEf
applyTestS (efRemoveAll [toV 'c']) $ \res ef -> do
res === ILM.empty
efFwd ef === multiMapV [('a', ""), ('b', "")]
efBwd ef === mapV []
-- remove singleton root
resetEf
applyTestS (efRemoveAll [toV 'b']) $ \res ef -> do
res === ILM.empty
efFwd ef === multiMapV [('a', "c")]
efBwd ef === mapV [('c', 'a')]
-- remove non-singleton root
resetEf
applyTestS (efRemoveAll [toV 'a']) $ \res ef -> do
res === mapV [('a', 'c')]
efFwd ef === multiMapV [('b', ""), ('c', "")]
efBwd ef === mapV []
-- remove all in class (root -> leaf order)
resetEf
applyTestS (efRemoveAll [toV 'a', toV 'c']) $ \res ef -> do
res === ILM.empty
efFwd ef === multiMapV [('b', "")]
efBwd ef === mapV []
-- remove all in class (leaf -> root order)
resetEf
applyTestS (efRemoveAll [toV 'c', toV 'a']) $ \res ef -> do
res === ILM.empty
efFwd ef === multiMapV [('b', "")]
efBwd ef === mapV []
-- remove with rotation and leaf
resetEf
addExtraEf
applyTestS (efRemoveAll [toV 'a']) $ \res ef -> do
res === mapV [('a', 'c')]
efFwd ef === multiMapV [('c', "d"), ('b', "")]
efBwd ef === mapV [('d', 'c')]
-- remove with two rotations
resetEf
addExtraEf
applyTestS (efRemoveAll [toV 'a', toV 'c']) $ \res ef -> do
res === mapV [('a', 'd')]
efFwd ef === multiMapV [('d', ""), ('b', "")]
efBwd ef === mapV []
-- remove with (leaf, rotation)
resetEf
addExtraEf
applyTestS (efRemoveAll [toV 'c', toV 'a']) $ \res ef -> do
res === mapV [('a', 'd')]
efFwd ef === multiMapV [('d', ""), ('b', "")]
efBwd ef === mapV []
testEfRec :: TestTree
testEfRec = testUnit "EF rec" $ runS efNew $ do
_ <- applyS (efAdd (toV 'a'))
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
applyTestS (efMerge (toV 'b') (toV 'c')) $ \res ef -> do
res === Just (toV 'b', setV "c")
efRootsSize ef === 2
efLeavesSize ef === 1
efTotalSize ef === 3
ILS.fromList (efRoots ef) === setV "ab"
ILS.fromList (efLeaves ef) === setV "c"
efFwd ef === multiMapV [('a', ""), ('b', "c")]
efBwd ef === mapV [('c', 'b')]
applyTestS (efMerge (toV 'a') (toV 'c')) $ \res ef -> do
res === Just (toV 'a', setV "bc")
efRootsSize ef === 1
efLeavesSize ef === 2
efTotalSize ef === 3
ILS.fromList (efRoots ef) === setV "a"
ILS.fromList (efLeaves ef) === setV "bc"
efFwd ef === multiMapV [('a', "bc")]
efBwd ef === mapV [('b', 'a'), ('c', 'a')]
testEfMany :: TestTree
testEfMany = testUnit "EF many" $ runS efNew $ do
_ <- applyS (efAdd (toV 'a'))
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
_ <- applyS (efAdd (toV 'd'))
_ <- applyS (efAdd (toV 'e'))
applyTestS (efMergeSets [setV "cde"]) $ \res ef -> do
res === Just (setV "c", setV "de")
efRootsSize ef === 3
efLeavesSize ef === 2
efTotalSize ef === 5
ILS.fromList (efRoots ef) === setV "abc"
ILS.fromList (efLeaves ef) === setV "de"
efFwd ef === multiMapV [('a', ""), ('b', ""), ('c', "de")]
efBwd ef === mapV [('d', 'c'), ('e', 'c')]
applyTestS (efMergeSets [setV "abd"]) $ \res ef -> do
res === Just (setV "a", setV "bcde")
efRootsSize ef === 1
efLeavesSize ef === 4
efTotalSize ef === 5
ILS.fromList (efRoots ef) === setV "a"
ILS.fromList (efLeaves ef) === setV "bcde"
efFwd ef === multiMapV [('a', "bcde")]
efBwd ef === mapV [('b', 'a'), ('c', 'a'), ('d', 'a'), ('e', 'a')]
testEfSets :: TestTree
testEfSets = testUnit "EF sets" $ runS efNew $ do
_ <- applyS (efAdd (toV 'a'))
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
_ <- applyS (efAdd (toV 'd'))
_ <- applyS (efAdd (toV 'e'))
applyTestS (efMergeSets [setV "cde", setV "abc"]) $ \res ef -> do
res === Just (setV "a", setV "bcde")
efRootsSize ef === 1
efLeavesSize ef === 4
efTotalSize ef === 5
ILS.fromList (efRoots ef) === setV "a"
efFwd ef === multiMapV [('a', "bcde")]
testEfCompact :: TestTree
testEfCompact = testUnit "EF compact" $ runS efNew $ do
_ <- applyS (efAdd (toV 'a'))
_ <- applyS (efAdd (toV 'b'))
_ <- applyS (efAdd (toV 'c'))
_ <- applyS (efAdd (toV 'd'))
_ <- applyS (efAdd (toV 'e'))
testS $ \ef -> assert (not (efCanCompact ef))
applyTestS (efMergeSets [setV "cde"]) $ \res ef -> do
res === Just (setV "c", setV "de")
efFwd ef === multiMapV [('a', ""), ('b', ""), ('c', "de")]
efBwd ef === mapV [('d', 'c'), ('e', 'c')]
assert (efCanCompact ef)
applyTestS efCompact $ \res ef -> do
efFwd ef === multiMapV [('a', ""), ('b', ""), ('c', "")]
efBwd ef === ILM.empty
res === multiMapV [('c', "de")]
assert (not (efCanCompact ef))
testEfUnit :: TestTree
testEfUnit = testGroup "EF unit" [testEfSimple, testEfRec, testEfMany, testEfSets, testEfCompact, testEfRemove]
genDistinctPairFromList :: Eq a => [a] -> Gen (a, a)
genDistinctPairFromList = \case
xs@(_:_:_) -> do
a <- Gen.element xs
b <- Gen.element (delete a xs)
pure (a, b)
_ -> error "List needs more than two elements"
genListOfDistinctPairs :: Eq a => Range Int -> [a] -> Gen [(a, a)]
genListOfDistinctPairs nOpsRange vs =
if length vs < 2
then pure []
else Gen.list nOpsRange (genDistinctPairFromList vs)
genV :: Int -> Gen V
genV maxElems =
let minVal = ord 'a'
maxVal = minVal + maxElems - 1
in fmap V (Gen.int (Range.linear minVal maxVal))
genMembers :: Int -> Gen [V]
genMembers maxElems = do
let nElemsRange = Range.linear 0 maxElems
minVal = ord 'a'
n <- Gen.int nElemsRange
pure (fmap (\i -> V (minVal + i)) [0..n-1])
mkInitEf :: [V] -> EF
mkInitEf vs = execState (for_ vs efAdd) efNew
mkPairsMergedEf :: [(V, V)] -> EF -> EF
mkPairsMergedEf vvs = execState (for_ vvs (uncurry efMerge))
mkSetsMergedEf :: [(V, V)] -> EF -> EF
mkSetsMergedEf vvs = execState (for_ (fmap (\(x, y) -> [ILS.fromList [x, y]]) vvs) efMergeSets)
mkSingleMergedEf :: [(V, V)] -> EF -> EF
mkSingleMergedEf vvs = execState (efMergeSets (fmap (\(x, y) -> ILS.fromList [x, y]) vvs))
data MergeStrat = MergeStratPairs | MergeStratSets | MergeStratSingle
deriving stock (Eq, Show, Enum, Bounded)
genMergeStrat :: Gen MergeStrat
genMergeStrat = Gen.enumBounded
testEfProp :: TestLimit -> TestTree
testEfProp lim = after AllSucceed "EF unit" $ testProp "EF prop" lim $ do
let maxElems = 50
-- generate elements
memberList <- forAll (genMembers maxElems)
let memberSet = ILS.fromList memberList
nMembers = ILS.size memberSet
allPairs = ILS.unorderedPairs memberSet
nOpsRange = Range.linear 0 (nMembers * nMembers)
let initEf = mkInitEf memberList
-- assert that sizes indicate nothing is merged
efRootsSize initEf === nMembers
efLeavesSize initEf === 0
efTotalSize initEf === nMembers
-- assert that find indicates nothing is merged
for_ allPairs $ \(a, b) -> flip evalStateT initEf $ do
x <- applyS (gets (efFindRoot a))
y <- applyS (gets (efFindRoot b))
assert (isJust x)
assert (isJust y)
x /== y
-- generate some pairs and merge them
mergePairs <- forAll (genListOfDistinctPairs nOpsRange memberList)
mergeStrat <- forAll genMergeStrat
let mergedUf =
case mergeStrat of
MergeStratPairs -> mkPairsMergedEf mergePairs initEf
MergeStratSets -> mkSetsMergedEf mergePairs initEf
MergeStratSingle -> mkSingleMergedEf mergePairs initEf
-- assert that total size is unchanged
efTotalSize mergedUf === nMembers
-- calculate components by graph reachability
let components = ILG.undirectedComponents mergePairs
-- assert that elements are equal or not according to component
_ <- foldS_ mergedUf allPairs $ \(a, b) -> do
x <- applyS (gets (efFindRoot a))
y <- applyS (gets (efFindRoot b))
let aComponent = ILE.lookupClass a components
bComponent = ILE.lookupClass b components
if isJust aComponent && aComponent == bComponent
then x === y
else x /== y
pure ()
type AV = Assoc ENodeId V
-- | Asserts assoc is compact - should also check 'assertAssocInvariants'
assertAssocCompact :: (MonadTest m, Eq a, Hashable a, Show a) => Assoc ENodeId a -> m ()
assertAssocCompact av = do
let fwd = assocFwd av
bwd = assocBwd av
-- Assert that the assoc has been rebuilt
assert $ not (assocCanCompact av)
-- Look at sizes to confirm that assoc could map 1-1
ILM.size fwd === HashMap.size bwd
-- Go through keys forward
for_ (ILM.toList fwd) $ \(x, fc) -> do
-- Assert is found in backward map AND maps back
HashMap.lookup fc bwd === Just x
-- Go through keys backward
for_ (HashMap.toList bwd) $ \(fc, x) ->
-- Assert is present in forward map AND maps back
ILM.lookup x fwd === Just fc
-- | Asserts assoc is correctly structured (compact or not)
assertAssocInvariants :: (MonadTest m, Eq a, Hashable a) => Assoc ENodeId a -> m ()
assertAssocInvariants av = do
let fwd = assocFwd av
bwd = assocBwd av
equiv = assocEquiv av
-- First check that fwd and bwd are 1-1
-- Go through keys forward
for_ (ILM.toList fwd) $ \(_, fc) -> do
-- Assert is found in backward map
assert $ HashMap.member fc bwd
-- Go through keys backward
for_ (HashMap.toList bwd) $ \(_, x) ->
-- Assert is present in forward map
assert $ ILM.member x fwd
-- Assert that fwd keys are exactly the equiv roots
ILS.fromList (ILM.keys fwd) === ILS.fromList (efRoots equiv)
data AssocCase = AssocCase !String ![(Int, Char)] ![(Int, Char, Int, AssocInsertRes Int)] ![(Int, Char)]
allAssocCases :: [AssocCase]
allAssocCases =
let start = [(0, 'a'), (1, 'b'), (2, 'c')]
in [ AssocCase "base" start [] start
, AssocCase "ident" start
[(0, 'a', 0, AssocInsertResUnchanged)]
start
, AssocCase "superfluous" start
[(4, 'a', 0, AssocInsertResMerged (ILS.singleton 4))]
start
, AssocCase "internal" start
[(0, 'b', 0, AssocInsertResMerged (ILS.singleton 1))]
[(0, 'b'), (2, 'c')]
, AssocCase "external" start
[(0, 'd', 0, AssocInsertResUpdated)]
[(0, 'd'), (1, 'b'), (2, 'c')]
, AssocCase "additional" start
[(4, 'd', 4, AssocInsertResCreated)]
[(0, 'a'), (1, 'b'), (2, 'c'), (4, 'd')]
, AssocCase "chain fwd" start
-- The singleton set in the second result is just the children (and self) of the clobbered node
-- We don't have to lookup the old clobbered nodes for 1 bc when this is used everything will be merged
[(0, 'b', 0, AssocInsertResMerged (ILS.singleton 1)), (1, 'c', 0, AssocInsertResMerged (ILS.singleton 2))]
[(0, 'c')]
, AssocCase "chain bwd" start
-- The set in the second result is not a singleton here because it already had children
[(1, 'c', 1, AssocInsertResMerged (ILS.singleton 2)), (0, 'c', 0, AssocInsertResMerged (ILS.fromList [1,2]))]
[(0, 'c')]
, AssocCase "chain self" start
[(1, 'c', 1, AssocInsertResMerged (ILS.singleton 2)), (2, 'c', 1, AssocInsertResUnchanged)]
[(0, 'a'), (1, 'c')]
, AssocCase "chain change" start
[(1, 'c', 1, AssocInsertResMerged (ILS.singleton 2)), (2, 'd', 1, AssocInsertResUpdated)]
[(0, 'a'), (1, 'd')]
, AssocCase "chain back id" start
[(1, 'c', 1, AssocInsertResMerged (ILS.singleton 2)), (1, 'b', 1, AssocInsertResUpdated)]
[(0, 'a'), (1, 'b')]
, AssocCase "chain back del" start
[(1, 'c', 1, AssocInsertResMerged (ILS.singleton 2)), (2, 'b', 1, AssocInsertResUpdated)]
[(0, 'a'), (1, 'b')]
, AssocCase "chain change rev" start
[(2, 'd', 2, AssocInsertResUpdated), (1, 'c', 1, AssocInsertResUpdated)]
[(0, 'a'), (1, 'c'), (2, 'd')]
]
mkAssoc :: [(Int, Char)] -> AV
mkAssoc rawPairs =
let pairs = fmap (bimap ENodeId toV) rawPairs
in assocFromList pairs
runAV :: Monad m => [(Int, Char)] -> StateT AV m () -> m ()
runAV = runS . mkAssoc
testAssocCase :: AssocCase -> TestTree
testAssocCase (AssocCase name start act end) = testUnit name $ runAV start $ do
testS $ \av -> do
assertAssocInvariants av
assertAssocCompact av
assocSize av === length start
for_ act $ \(x, a, expectedY, expectedRes) -> do
(actualY, actualRes) <- applyS (assocInsert (ENodeId x) (toV a))
(actualY, actualRes) === coerce (expectedY, expectedRes)
testS assertAssocInvariants
_ <- applyS assocCompact
testS $ \av -> do
assertAssocInvariants av
assertAssocCompact av
assocSize av === length end
let endAv = mkAssoc end
assocFwd av === assocFwd endAv
assocBwd av === assocBwd endAv
testAssocCases :: TestTree
testAssocCases = testGroup "Assoc case" (fmap testAssocCase allAssocCases)
testAssocUnit :: TestTree
testAssocUnit = testUnit "Assoc unit" $ do
let a0 = assocNew :: AV
assertAssocInvariants a0
assertAssocCompact a0
assocSize a0 === 0
let aKey = ENodeId 0
aVal = toV 'a'
bKey = ENodeId 1
bVal = toV 'b'
cKey = ENodeId 2
cVal = toV 'c'
let members = [(aKey, aVal), (bKey, bVal), (cKey, cVal)]
let a1 = execState (for_ members (uncurry assocInsert)) a0
assertAssocInvariants a1
assertAssocCompact a0
assocSize a1 === 3
assocRoots a1 === [aKey, bKey, cKey]
assocLeaves a1 === []
let (res, a2) = runState (assocInsert aKey bVal) a1
res === (aKey, AssocInsertResMerged (ILS.singleton bKey))
assertAssocInvariants a2
assert $ assocCanCompact a2
assocSize a2 === 2
assocRoots a2 === [aKey, cKey]
assocLeaves a2 === [bKey]
let a3 = execState assocCompact a2
assertAssocInvariants a3
assertAssocCompact a3
assocSize a3 === 2
assocRoots a3 === [aKey, cKey]
assocLeaves a3 === []
type EGA = EGraph () ArithF
type EGP = Pat ArithF String
testEgUnit :: TestTree
testEgUnit = after AllSucceed "Assoc unit" $ testUnit "EG unit" $ runS egNew $ do
-- We're going to have our egraph track the equality `2 + 2 = 4`.
-- We disable analysis
let ana = noAnalysis
-- Some simple terms:
let termFour = ArithConst 4
termTwo = ArithConst 2
termPlus = ArithPlus termTwo termTwo
-- And a simple pattern:
let pat = FreeEmbed (ArithPlusF (FreePure "x") (FreePure "y")) :: EGP
-- Test that the empty egraph is sane
testS $ \eg -> do
egClassSize eg === 0
egNodeSize eg === 0
-- Nothing is matched
testS $ \eg ->
match pat eg === []
-- Add the term `4`
cidFour <- applyTestS (egAddTerm ana termFour) $ \(c, x) eg -> do
c === ChangedYes
egFindTerm termFour eg === Just x
egClassSize eg === 1
egNodeSize eg === 1
pure x
-- Add the term `2`
cidTwo <- applyTestS (egAddTerm ana termTwo) $ \(c, x) eg -> do
c === ChangedYes
x /== cidFour
egFindTerm termTwo eg === Just x
egClassSize eg === 2
egNodeSize eg === 2
pure x
-- Add the term `4` again and assert things haven't changed
applyTestS (egAddTerm ana termFour) $ \(c, x) eg -> do
c === ChangedNo
x === cidFour
egFindTerm termFour eg === Just x
egClassSize eg === 2
egNodeSize eg === 2
-- Still, nothing is matched
testS $ \eg ->
match pat eg === []
-- Add the term `2 + 2`
cidPlus <- applyTestS (egAddTerm ana termPlus) $ \(c, x) eg -> do
c === ChangedYes
x /== cidFour
x /== cidTwo
egFindTerm termPlus eg === Just x
egClassSize eg === 3
egNodeSize eg === 3
pure x
-- We now match `2 + 2`
testS $ \eg ->
match pat eg ===
[ MatchSubst
(Match cidPlus
(MatchPatEmbed
(ArithPlusF
(Match cidTwo (MatchPatPure "x"))
(Match cidTwo (MatchPatPure "y"))
)
)
)
(HashMap.fromList [("x", cidTwo), ("y", cidTwo)])
]
-- Merge `4` and `4` and assert things haven't changed
applyTestS (egMerge cidFour cidFour) $ \m _ -> do
case m of
MergeResultUnchanged -> pure ()
_ -> fail "expected unchanged merge"
-- Merge `2 + 2` and `4`
applyTestS (egMerge cidPlus cidFour) $ \m eg -> do
case m of
MergeResultChanged _ -> pure ()
_ -> fail "expected changed merge"
egFindTerm termFour eg === Just cidFour
egFindTerm termPlus eg === Just cidFour
egFindTerm termTwo eg === Just cidTwo
-- We still match `2 + 2`, but the class is different
testS $ \eg ->
match pat eg ===
[ MatchSubst
(Match cidFour
(MatchPatEmbed
(ArithPlusF
(Match cidTwo (MatchPatPure "x"))
(Match cidTwo (MatchPatPure "y"))
)
)
)
(HashMap.fromList [("x", cidTwo), ("y", cidTwo)])
]
type EGD = Max V
type EGF = BinTreeF V
type EGT = BinTree V
type EGV = EGraph EGD EGF
maxVAnalysis :: EAnalysis EGD EGF
maxVAnalysis = \case
BinTreeLeafF v -> Max v
BinTreeBranchF d1 d2 -> d1 <> d2
assertEgInvariants :: (MonadTest m, Traversable f, Eq (f EClassId), Hashable (f EClassId), Show (f EClassId)) => EGraph d f -> m ()
assertEgInvariants eg = do
let assoc = egNodeAssoc eg
hc = egHashCons eg
bwd = assocBwd assoc
rootNodes = ILS.fromList (assocRoots assoc)
leafNodes = ILS.fromList (assocLeaves assoc)
allNodes = ILS.union rootNodes leafNodes
ef = egEquivFind eg
rootClasses = ILS.fromList (efRoots ef)
leafClasses = ILS.fromList (efLeaves ef)
cm = egClassMap eg
cmClasses = ILS.fromList (ILM.keys cm)
-- Assert that root nodes and leaf nodes are disjoint
ILS.intersection rootNodes leafNodes === ILS.empty
-- Assert that root classes and leaf classes are disjoint
ILS.intersection rootClasses leafClasses === ILS.empty
-- Assert that the assoc is 1-1 etc
assertAssocInvariants assoc
-- Assert that the hashcons and assoc have equal key sets
ILS.fromList (ILM.keys hc) === allNodes
-- Assert that hashcons has exactly the same values as unionfind roots for all nodes
for_ (ILM.elems hc) $ \c ->
assert $ ILS.member c rootClasses
-- Assert that classmap only contains unionfind roots
cmClasses === rootClasses
-- For every node, assert in the nodes of some class
for_ (ILM.toList hc) $ \(n, c) -> do
let nodes = eciNodes (ILM.partialLookup c cm)
assert (assocMember n nodes)
-- For every root, assert is in all parent classes
for_ (ILM.toList hc) $ \(n, c) ->
when (ILS.member n rootNodes) $ do
-- for all children that are not of the node's own class
let children = ILS.filter (/= c) (foldMap ILS.singleton (assocPartialLookupByKey n assoc))
for_ (ILS.toList children) $ \y -> do
-- look up child and assert in child's parents
let parents = eciParents (ILM.partialLookup y cm)
assert (ILS.member n parents)
-- For every class
cmNodes <- flipFoldM ILS.empty (ILM.toList cm) $ \accNodesSet (c, eci) -> do
let nodes = eciNodes eci
nodesSet = ILS.fromList (assocMembers nodes)
parents = eciParents eci
-- Assert that classmap node values are non-empty
nodesSet /== ILS.empty
-- Assert that classmap class has node values that are hashconsed to class
for_ (ILS.toList nodesSet) $ \n -> do
ILM.lookup n hc === Just c
-- Assert that classmap class has NO parents that are hashconsed to class
for_ (ILS.toList parents) $ \p ->
ILM.lookup p hc /== Just c
-- Assert we haven't seen these nodes before
assert $ ILS.disjoint nodesSet accNodesSet
-- Assert that the nodes and parents are disjoint
assert $ ILS.disjoint nodesSet parents
pure (ILS.union accNodesSet nodesSet)
let hcNodes = ILS.fromList (ILM.keys hc)
-- Assert hc keys are exactly the class nodes
cmNodes === hcNodes
-- Now test recanonicalization - we already know assoc fwd and bwd are 1-1
for_ (HashMap.toList bwd) $ \(fc, _) ->
let recanon = evalState (egCanonicalize fc) eg
in recanon === Right fc
data EgRound = EgRound
{ egRoundTerms :: ![EGT]
, egRoundSets :: ![[EGT]]
, egRoundEqTests :: ![[EGT]]
, egRoundNeqTests :: ![(EGT, EGT)]
} deriving stock (Eq, Show)
data EgCase = EgCase
{ egCaseName :: !String
, egCaseRounds :: ![EgRound]
} deriving stock (Eq, Show)
allEgCases :: [EgCase]
allEgCases =
let leafA = BinTreeLeaf (toV 'a')
leafB = BinTreeLeaf (toV 'b')
leafC = BinTreeLeaf (toV 'c')
leafD = BinTreeLeaf (toV 'd')
leafE = BinTreeLeaf (toV 'e')
leafTerms = [leafA, leafB, leafC, leafD]
parentAA = BinTreeBranch leafA leafA
parentAB = BinTreeBranch leafA leafB
parentAC = BinTreeBranch leafA leafC
parentAD = BinTreeBranch leafA leafD
parentBD = BinTreeBranch leafB leafD
parentCA = BinTreeBranch leafC leafA
simpleParentTerms = [parentAC, parentAD]
complexParentTerms = [parentAC, parentBD]
grandparentAAC = BinTreeBranch leafA parentAC
grandparentAAD = BinTreeBranch leafA parentAD
grandparentBAC = BinTreeBranch leafB parentAC
grandparentEAD = BinTreeBranch leafE parentAD
simpleGrandparentTerms = [grandparentAAC, grandparentAAD]
complexGrandparentTerms = [grandparentBAC, grandparentEAD]
in [ EgCase "simple"
[ EgRound leafTerms [] [] [(leafA, leafB), (leafA, leafC), (leafB, leafC)]
, EgRound [] [[leafA, leafB]] [[leafA, leafB]] [(leafA, leafC), (leafB, leafC)]
]
, EgCase "transitive one round"
[ EgRound leafTerms [] [] [(leafA, leafB), (leafA, leafC), (leafB, leafC), (leafA, leafD)]
, EgRound [] [[leafA, leafB], [leafB, leafC]] [[leafA, leafB, leafC]] [(leafA, leafD)]
]
, EgCase "transitive two round"
[ EgRound leafTerms [] [] [(leafA, leafB), (leafA, leafC), (leafB, leafC), (leafA, leafD)]
, EgRound [] [[leafA, leafB]] [[leafA, leafB]] [(leafA, leafC), (leafA, leafD)]
, EgRound [] [[leafB, leafC]] [[leafA, leafB, leafC]] [(leafA, leafD)]
]
, EgCase "simple parents"
[ EgRound simpleParentTerms [] [] [(leafC, leafD), (parentAC, parentAD)]
, EgRound [] [[leafC, leafD]] [[parentAC, parentAD]] []
]
, EgCase "complex parents one round"
[ EgRound complexParentTerms [] [] [(leafA, leafB), (leafC, leafD), (parentAC, parentBD)]
, EgRound [] [[leafA, leafB], [leafC, leafD]] [[leafA, leafB], [leafC, leafD], [parentAC, parentBD]] []
]
, EgCase "complex parents two round"
[ EgRound complexParentTerms [] [] [(leafA, leafB), (leafC, leafD), (parentAC, parentBD)]
, EgRound [] [[leafA, leafB]] [[leafA, leafB]] [(leafC, leafD), (parentAC, parentBD)]
, EgRound [] [[leafC, leafD]] [[leafA, leafB], [leafC, leafD], [parentAC, parentBD]] []
]
, EgCase "simple grandparents"
[ EgRound simpleGrandparentTerms [] [] [(leafC, leafD), (parentAC, parentAD), (grandparentAAC, grandparentAAD)]
, EgRound [] [[leafC, leafD]] [[leafC, leafD], [parentAC, parentAD], [grandparentAAC, grandparentAAD]] []
]
, EgCase "complex grandparents bottom up"
[ EgRound complexGrandparentTerms [] [] [(leafC, leafD), (leafB, leafE), (parentAC, parentAD), (grandparentBAC, grandparentEAD)]
, EgRound [] [[leafC, leafD]] [[leafC, leafD], [parentAC, parentAD]] [(leafB, leafE), (grandparentBAC, grandparentEAD)]
, EgRound [] [[leafB, leafE]] [[leafC, leafD], [leafB, leafE], [parentAC, parentAD], [grandparentBAC, grandparentEAD]] []
]
, EgCase "complex grandparents top down"
[ EgRound complexGrandparentTerms [] [] [(leafC, leafD), (leafB, leafE), (parentAC, parentAD), (grandparentBAC, grandparentEAD)]
, EgRound [] [[leafB, leafE]] [[leafB, leafE]] [(leafC, leafD), (parentAC, parentAD), (grandparentBAC, grandparentEAD)]
, EgRound [] [[leafC, leafD]] [[leafC, leafD], [leafB, leafE], [parentAC, parentAD], [grandparentBAC, grandparentEAD]] []
]
, EgCase "connect"
[ EgRound leafTerms [] [] [(leafA, leafB), (leafA, leafC), (leafB, leafC), (leafA, leafD)]
, EgRound [] [[leafA, leafB]] [[leafA, leafB]] [(leafA, leafC), (leafA, leafD)]
, EgRound [] [[leafC, leafD]] [[leafA, leafB], [leafC, leafD]] [(leafA, leafD)]
, EgRound [] [[leafB, leafD]] [[leafA, leafB, leafC, leafD]] []
]
, EgCase "mid grandparents"
[ EgRound simpleGrandparentTerms [] [] [(leafC, leafD), (parentAC, parentAD), (grandparentAAC, grandparentAAD)]
, EgRound [] [[parentAC, parentAD]] [[parentAC, parentAD], [grandparentAAC, grandparentAAD]] [(leafC, leafD)]
, EgRound [] [[leafC, leafD]] [[leafC, leafD], [parentAC, parentAD], [grandparentAAC, grandparentAAD]] []
]
, EgCase "unify node"
[ EgRound [BinTreeBranch parentAC leafA, parentAA] [] [] [(parentAC, parentAA)]
, EgRound [] [[leafA, leafC]] [[parentAC, parentAA]] []
]
, EgCase "self parent"
[ EgRound [BinTreeBranch parentAC leafB] [] [] []
, EgRound [] [[parentAC, leafA]] [] []
]
, EgCase "self parent again"
[ EgRound [leafB, parentAA] [] [] []
, EgRound [] [[leafB, leafA], [leafB, parentAA]] [] []
]
, EgCase "dead add parent"
[ EgRound [parentAC, leafB] [[leafA, parentAC], [leafA, leafC]] [[parentAC, leafC]] [(leafA, leafB)]
, EgRound [parentCA] [] [[parentCA, parentAC]] []
]
, EgCase "repro 1"
[ EgRound [BinTreeBranch parentAA parentAB] [[leafB, leafA]] [] [(leafB, parentAA)]
, EgRound [leafA] [[parentAA, leafA]] [[leafB, parentAA]] []
]
, let grandparent = BinTreeBranch parentAA leafB
greatGrandparent = BinTreeBranch grandparent leafA
in EgCase "repro 2"
[ EgRound [greatGrandparent, leafA, leafC] [[greatGrandparent, parentAA], [leafA, leafB]] [] []
, EgRound [leafA, leafA] [[leafA, grandparent]] [] []
, EgRound [leafA, leafA, leafA] [[parentAA, leafA]] [] []
]
]
testEgCase :: EgCase -> TestTree
testEgCase (EgCase name rounds) = kase where
findMayTerm t = fmap (egFindTerm t) get
findTerm t = fmap fromJust (findMayTerm t)
findTerms ts = fmap ILS.fromList (for ts findTerm)
assertTermFound t = findMayTerm t >>= \mi -> assert (isJust mi)
assertTermsFound ts = for_ ts assertTermFound
kase = testUnit name $ runS egNew $ do
-- for each round
for_ rounds $ \(EgRound start act endEq endNeq) -> do
-- add initial terms and assert invariants hold
applyS (for_ start (egAddTerm maxVAnalysis))
-- liftIO (putStrLn "===== post add =====")
-- testS $ liftIO . pPrint
testS assertEgInvariants
-- assert that all mentioned terms are in the egraph
assertTermsFound start
for_ act assertTermsFound
for_ endEq assertTermsFound
for_ endNeq $ \(x, y) -> do
-- also assert that neq terms are not themselves equal
x /== y
assertTermFound x
assertTermFound y
-- merge sets of terms and rebuild
applyS $ do
sets <- for act findTerms
mr <- egMergeMany (Seq.fromList sets)
case mr of
MergeResultMissing _ -> error "bad set"
_ -> pure ()
-- assert invariants hold
testS assertEgInvariants
-- find merged terms again and assert they are in same classes
sets <- applyS $ for act findTerms
for_ sets $ \set -> ILS.size set === 1
-- find final eq terms and assert they are in same classes
for_ endEq $ \ts -> do
set <- applyS (findTerms ts)
ILS.size set === 1
-- find final neq terms and assert they are not in same class
for_ endNeq $ \(x, y) -> do
i <- applyS (findTerm x)
j <- applyS (findTerm y)
i /== j
testEgCases :: TestTree
testEgCases = testGroup "Eg case" $
testEgCase <$> allEgCases
testEgNew :: TestTree
testEgNew = testUnit "EG new" $ do
eg0 <- fullyEvaluate (egNew :: EGV)
egNodeSize eg0 === 0
egClassSize eg0 === 0
assertEgInvariants eg0
genNodePairs :: Range Int -> EGV -> Gen [(EClassId, EClassId)]
genNodePairs nOpsRange eg = genListOfDistinctPairs nOpsRange (ILM.keys (egClassMap eg))
genSomeList :: [a] -> Gen [a]
genSomeList xs = go where
go = Gen.recursive Gen.choice [Gen.constant [], fmap pure (Gen.element xs)] [Gen.subterm2 go go (++)]
genBinTree :: Gen a -> Gen (BinTree a)
genBinTree genA = genEither where
genLeaf = fmap BinTreeLeaf genA
genBranch = Gen.subterm2 genEither genEither BinTreeBranch
genEither = Gen.recursive Gen.choice [genLeaf] [genBranch]
genBinTreeMembers :: Int -> Gen [BinTree V]
genBinTreeMembers maxElems = Gen.list (Range.linear 0 maxElems) (genBinTree (genV maxElems))
-- An alternative to 'genBinTreeMembers' that makes smaller trees
mkSimpleTreeLevels :: Int -> [BinTree V]
mkSimpleTreeLevels maxElems =
let letters = take maxElems (['a'..'z'] ++ ['A'..'Z'])
zeroLevel = fmap (BinTreeLeaf . toV) letters
mkLevel y x = (BinTreeBranch <$> x <*> y) ++ (BinTreeBranch <$> y <*> x)
mkLevels y = foldr (\x r -> mkLevel y x ++ r) (BinTreeBranch <$> y <*> y)
oneLevel = mkLevels zeroLevel []
twoLevel = mkLevels oneLevel [zeroLevel]
anyLevel = zeroLevel ++ oneLevel ++ twoLevel
in anyLevel
testEgProp :: TestLimit -> TestTree
testEgProp lim = after AllSucceed "EG unit" $ after AllSucceed "EG cases" $ testProp "EG prop" lim prop where
maxElems = 10
termGen = genBinTreeMembers maxElems
-- Guarantee yourself small trees with this:
-- termGen = genSomeList (mkSimpleTreeLevels maxElems)
prop = do
eg0 <- fullyEvaluate (egNew :: EGV)
rounds <- forAll (Gen.element [1, 2, 3])
body rounds eg0
body (rounds :: Int) eg0 = do
members <- forAll termGen
let nMembers = length members
nOpsRange = Range.linear 0 (nMembers * nMembers)
eg1 <- fullyEvaluate (execState (for_ members (egAddTerm maxVAnalysis)) eg0)
assertEgInvariants eg1
pairs <- forAll (genNodePairs nOpsRange eg1)
let merge = do
mr <- egMergeMany (Seq.fromList (fmap (\(a, b) -> ILS.fromList [a, b]) pairs))
case mr of
MergeResultMissing _ -> error "bad set"
_ -> pure ()
eg2 <- fullyEvaluate (execState merge eg1)
assertEgInvariants eg2
unless (rounds == 1) (body (rounds - 1) eg2)
type M = IntLikeMap ENodeId Char
testILM :: TestTree
testILM = testUnit "ILM unit" $ do
let mLeft = ILM.fromList [(ENodeId 0, 'a'), (ENodeId 1, 'b')] :: M
mRight = ILM.fromList [(ENodeId 1, 'x'), (ENodeId 2, 'c')] :: M
mMerged = ILM.fromList [(ENodeId 0, 'a'), (ENodeId 1, 'b'), (ENodeId 2, 'c')] :: M
mLeft <> mRight === mMerged
main :: IO ()
main = testMain $ \lim -> testGroup "Overeasy"
[ testILM
, testEfUnit
, testAssocUnit
, testAssocCases
, testEgUnit
, testEgNew
, testEgCases
, testEfProp lim
, testEgProp lim
]