souffle-haskell-3.2.0: tests/Test/Language/Souffle/AnalysisSpec.hs
{-# LANGUAGE DataKinds, TypeFamilies, DeriveGeneric, Arrows #-}
module Test.Language.Souffle.AnalysisSpec
( module Test.Language.Souffle.AnalysisSpec
) where
import Prelude hiding ((.), id)
import Control.Arrow
import Control.Category
import Test.Hspec
import Data.Profunctor
import GHC.Generics
import Control.Monad.IO.Class
import Language.Souffle.Analysis
import qualified Language.Souffle.Interpreted as Souffle
data Path = Path
data Edge = Edge String String
deriving (Eq, Show, Generic)
data Reachable = Reachable String String
deriving (Eq, Show, Generic)
instance Souffle.Program Path where
type ProgramFacts Path = [Edge, Reachable]
programName = const "path"
instance Souffle.Fact Edge where
type FactDirection Edge = 'Souffle.InputOutput
factName = const "edge"
instance Souffle.Fact Reachable where
type FactDirection Reachable = 'Souffle.Output
factName = const "reachable"
instance Souffle.Marshal Edge
instance Souffle.Marshal Reachable
data Results = Results [Reachable] [Edge]
deriving (Eq, Show)
pathAnalysis :: Souffle.Handle Path
-> Analysis Souffle.SouffleM [Edge] [Reachable]
pathAnalysis h =
mkAnalysis (Souffle.addFacts h) (Souffle.run h) (Souffle.getFacts h)
-- A little bit silly, but good enough to test different forms of application with
pathAnalysis' :: Souffle.Handle Path
-> Analysis Souffle.SouffleM [Edge] [Edge]
pathAnalysis' h =
mkAnalysis (Souffle.addFacts h) (Souffle.run h) (Souffle.getFacts h)
data RoundTrip = RoundTrip
newtype StringFact = StringFact String
deriving (Eq, Show, Generic)
instance Souffle.Program RoundTrip where
type ProgramFacts RoundTrip = '[StringFact]
programName = const "round_trip"
instance Souffle.Fact StringFact where
type FactDirection StringFact = 'Souffle.InputOutput
factName = const "string_fact"
instance Souffle.Marshal StringFact
roundTripAnalysis :: Souffle.Handle RoundTrip
-> Analysis Souffle.SouffleM [Reachable] [StringFact]
roundTripAnalysis h =
mkAnalysis addFacts (Souffle.run h) (Souffle.getFacts h)
where
addFacts rs = do
Souffle.addFacts h $ map (\(Reachable a _) -> StringFact a) rs
withSouffle :: Souffle.Program a => a -> (Souffle.Handle a -> Souffle.SouffleM ()) -> IO ()
withSouffle prog f = Souffle.runSouffle prog $ \case
Nothing -> error "Failed to load program"
Just h -> f h
edges :: [Edge]
edges = [Edge "a" "b", Edge "b" "c", Edge "b" "d", Edge "d" "e"]
spec :: Spec
spec = describe "composing analyses" $ parallel $ do
it "supports fmap" $ do
withSouffle Path $ \h -> do
let analysis = pathAnalysis h
analysis' = fmap length analysis
count <- execAnalysis analysis' edges
liftIO $ count `shouldBe` 8
describe "analysis used as a profunctor" $ parallel $ do
it "supports lmap" $ do
withSouffle Path $ \h -> do
let inputs = [("a", "b"), ("b", "c")]
analysis = pathAnalysis h
analysis' = lmap (map (uncurry Edge)) analysis
rs <- execAnalysis analysis' inputs
liftIO $ rs `shouldBe` [ Reachable "a" "b"
, Reachable "a" "c"
, Reachable "b" "c"
]
it "supports rmap" $ do
withSouffle Path $ \h -> do
let analysis = pathAnalysis h
analysis' = rmap length analysis
count <- execAnalysis analysis' edges
liftIO $ count `shouldBe` 8
it "supports applicative composition" $
withSouffle Path $ \hPath -> do
let analysis1 = pathAnalysis hPath
analysis2 = pathAnalysis' hPath
analysis = Results <$> analysis1 <*> analysis2
inputs = [Edge "a" "b", Edge "b" "c"]
reachables = [ Reachable "a" "b"
, Reachable "a" "c"
, Reachable "b" "c"
]
results <- execAnalysis analysis inputs
liftIO $ results `shouldBe` Results reachables inputs
it "supports semigroupal composition" $ do
withSouffle Path $ \h -> do
let analysis = pathAnalysis h
analysis' = analysis <> analysis
rs <- execAnalysis analysis' [Edge "a" "b", Edge "b" "c"]
let results = [ Reachable "a" "b"
, Reachable "a" "c"
, Reachable "b" "c"
]
results' = mconcat $ replicate 2 results
liftIO $ rs `shouldBe` results'
it "supports mempty" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM [Edge] [Reachable]
analysis = mempty
rs <- execAnalysis analysis [Edge "a" "b", Edge "b" "c"]
liftIO $ rs `shouldBe` []
it "supports converting an analysis to a monadic function" $ do
withSouffle Path $ \h -> do
let analysis = pathAnalysis h
rs <- execAnalysis analysis [Edge "a" "b", Edge "b" "c"]
let results = [ Reachable "a" "b"
, Reachable "a" "c"
, Reachable "b" "c"
]
liftIO $ rs `shouldBe` results
describe "analysis used as a category" $ parallel $ do
it "supports 'id'" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM [Edge] [Edge]
analysis = id
edges' <- execAnalysis analysis edges
liftIO $ edges' `shouldBe` edges
it "supports sequential composition using (.)" $ do
withSouffle Path $ \h -> do
let reachableToFlippedEdge (Reachable a b) = Edge b a
analysis1 = pathAnalysis h
analysis2 = lmap (map reachableToFlippedEdge) $ pathAnalysis h
rs <- execAnalysis (analysis2 . analysis1) [Edge "a" "b", Edge "b" "c"]
let results = [ Reachable "a" "a"
, Reachable "a" "b"
, Reachable "a" "c"
, Reachable "b" "a"
, Reachable "b" "b"
, Reachable "b" "c"
, Reachable "c" "a"
, Reachable "c" "b"
, Reachable "c" "c"
]
liftIO $ rs `shouldBe` results
describe "analysis used as an arrow" $ parallel $ do
it "supports 'arr'" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM Int Int
analysis = arr (+1)
result1 <- execAnalysis analysis 41
result2 <- execAnalysis (arr id) 41
liftIO $ result1 `shouldBe` 42
liftIO $ result2 `shouldBe` 41
it "supports 'first'" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM (Int, Bool) (Int, Bool)
analysis = first (arr (+1))
input = (41, True)
result <- execAnalysis analysis input
liftIO $ result `shouldBe` (42, True)
it "supports 'second'" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM (Bool, Int) (Bool, Int)
analysis = second (arr (+1))
input = (True, 41)
result <- execAnalysis analysis input
liftIO $ result `shouldBe` (True, 42)
it "supports (***)" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM (Bool, Int) (Bool, Int)
analysis = arr not *** arr (+1)
input = (True, 41)
result <- execAnalysis analysis input
liftIO $ result `shouldBe` (False, 42)
it "supports (&&&)" $ do
withSouffle Path $ \_ -> do
let analysis :: Analysis Souffle.SouffleM Int (Bool, Int)
analysis = arr (== 1000) &&& arr (+1)
input = 41
result <- execAnalysis analysis input
liftIO $ result `shouldBe` (False, 42)
it "supports arrow notation" $ do
withSouffle Path $ \h -> do
liftIO $ withSouffle RoundTrip $ \h' -> do
let arrowAnalysis = proc es -> do
rs <- pathAnalysis h -< es
strs <- roundTripAnalysis h' -< rs
returnA -< strs
result <- execAnalysis arrowAnalysis edges
liftIO $ result `shouldBe` [ StringFact "a"
, StringFact "b"
, StringFact "d"
]
it "supports case expressions in arrow notation" $ do
withSouffle Path $ \h -> do
let analysis = proc es -> do
rs <- pathAnalysis h -< es
case rs of
[] -> returnA -< []
rs' -> returnA -< take 2 rs'
result <- execAnalysis analysis edges
let expected = [ Reachable "a" "b", Reachable "a" "c" ]
liftIO $ result `shouldBe` expected