packages feed

crucible-0.7: test/absint/WTO.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
module WTO (
  wtoTests
  ) where

import Control.Applicative
import Control.Monad ( replicateM, unless )
import qualified Control.Monad.State.Strict as St
import qualified Data.Foldable as F
import qualified Data.Map.Strict as M
import qualified Data.Set as S

import Prelude

import qualified Test.QuickCheck as QC
import qualified Test.Tasty as T
import qualified Test.Tasty.QuickCheck as T

import Lang.Crucible.Analysis.Fixpoint.Components

wtoTests :: T.TestTree
wtoTests = T.testGroup "WeakTopologicalOrdering" [
  T.testProperty "prop_reachableInWTO" prop_reachableInWTO,
  T.testProperty "prop_validWTO" prop_validWTO
  ]

-- | Test that all reachable nodes in the graph are present in the
-- weak topological ordering
prop_reachableInWTO :: RandomGraph -> Bool
prop_reachableInWTO gp = dfsReachedNodes dfs == S.fromList (concatMap F.toList wto)
  where
    dfs = reachable gp
    (root, sf) = toCFG gp
    wto = weakTopologicalOrdering sf root

-- | Test that the weak topological ordering property holds for the
-- WTO we compute.
--
-- That property is defined in terms of a relation w(c), which
-- evaluates to the set of heads of the nested components containing
-- the vertex c.
--
-- For every edge u->v:
--
--   (u < v AND v not in w(u)) OR (v <= u AND v in w(u))
--
-- where v <= u means that u->v is a backedge.
prop_validWTO :: RandomGraph -> Bool
prop_validWTO gp@(RG edges) =
  and [ (isBackEdge e && (v `vertexInWC` u)) || not (v `vertexInWC` u)
      | e@(u, v) <- edges
      , isReachableEdge e
      ]
  where
    dfs = reachable gp
    (root, sf) = toCFG gp
    wto = weakTopologicalOrdering sf root
    cchs = indexContainingComponentHeads wto
    isReachableEdge (src, _) = src `S.member` dfsReachedNodes dfs
    isBackEdge e = e `S.member` dfsBackEdges dfs
    vertexInWC v c = maybe False (S.member v) $ M.lookup c cchs

-- | This is the w(c) relation from the WTO criteria
--
-- The map keys are the cs
indexContainingComponentHeads :: (Ord n) => [WTOComponent n] -> M.Map n (S.Set n)
indexContainingComponentHeads cs = St.execState (mapM_ (go []) cs) M.empty
  where
    go heads c =
      case c of
        Vertex v -> St.modify' $ M.insert v (S.fromList heads)
        SCC (SCCData { wtoHead = h
                     , wtoComps = cs'
                     }) -> do
          let heads' = h : heads
          St.modify' $ M.insert h (S.fromList heads')
          mapM_ (go heads') cs'

newtype NodeId = NID Int
  deriving (Eq, Show)

instance QC.Arbitrary NodeId where
  arbitrary = QC.sized mkNodeId
    where
      mkNodeId n = NID <$> QC.choose (0, n)

newtype RandomGraph = RG [(Int, Int)]
  deriving (Show)

instance QC.Arbitrary RandomGraph where
  arbitrary = QC.sized mkRandomGraph

-- | Make an arbitrary graph by deciding on a number of edges and then
-- generating random edges.  Note that we always increment the size so
-- that we don't get empty graphs.
--
-- The graphs are not all connected.
mkRandomGraph :: Int -> QC.Gen RandomGraph
mkRandomGraph ((+1) -> sz) = do
  nEdges <- QC.choose (2, 2*sz)
  srcs <- replicateM nEdges (QC.choose (0, sz))
  dsts <- replicateM nEdges (QC.choose (0, sz))
  return $! RG (unique (zip srcs dsts))

-- | A DFS result; it additionally contains a list of back edges
-- discovered during its traversal
data DFS = DFS { dfsReachedNodes :: S.Set Int
               , dfsBackEdges :: S.Set (Int, Int)
               , dfsStart :: S.Set Int
               , dfsFinish :: S.Set Int
               }

-- | Compute the DFS of a random graph from its root.
reachable :: RandomGraph -> DFS
reachable gp =
  St.execState (go root) s0
  where
    s0 = DFS { dfsReachedNodes = S.empty
             , dfsBackEdges = S.empty
             , dfsStart = S.empty
             , dfsFinish = S.empty
             }
    (root, sf) = toCFG gp
    go n = do
      markDiscovered n
      F.forM_ (sf n) $ \successor -> do
        disc <- isDiscovered successor
        case disc of
          False -> go successor
          True -> do
            fin <- isFinished successor
            unless fin $ addBackedge (n, successor)
      markFinished n

type M a = St.State DFS a

markDiscovered :: Int -> M ()
markDiscovered v = St.modify' $ \s ->
  s { dfsReachedNodes = S.insert v (dfsReachedNodes s)
    , dfsStart = S.insert v (dfsStart s)
    }

markFinished :: Int -> M ()
markFinished v = St.modify' $ \s ->
  s { dfsFinish = S.insert v (dfsFinish s) }

addBackedge :: (Int, Int) -> M ()
addBackedge be = St.modify' $ \s -> s { dfsBackEdges = S.insert be (dfsBackEdges s) }

isDiscovered :: Int -> M Bool
isDiscovered n = S.member n <$> St.gets dfsReachedNodes

isFinished :: Int -> M Bool
isFinished n = S.member n <$> St.gets dfsFinish

unique :: (Ord a) => [a] -> [a]
unique = S.toList . S.fromList

-- | Return the root and the successor function for the graph.
--
-- Not defined for empty graphs
toCFG :: RandomGraph -> (Int, (Int -> [Int]))
toCFG (RG []) = error "Empty graph"
toCFG (RG edges@((s0, _) : _)) =
  (s0, \n -> [ dst | (src, dst) <- edges, n == src])