packages feed

crucible-0.7: src/Lang/Crucible/Simulator/PathSplitting.hs

-----------------------------------------------------------------------
-- |
-- Module           : Lang.Crucible.Simulator.PathSplitting
-- Description      : Support for implementing path splitting
-- Copyright        : (c) Galois, Inc 2019
-- License          : BSD3
-- Maintainer       : Rob Dockins <rdockins@galois.com>
-- Stability        : provisional
--
-- This module provides an execution feature that converts symbolic
-- branches into path splitting by pushing unexplored paths onto a
-- worklist instead of performing eager path merging (the default
-- behavior).
------------------------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
module Lang.Crucible.Simulator.PathSplitting
  ( WorkItem(..)
  , WorkList
  , queueWorkItem
  , dequeueWorkItem
  , restoreWorkItem
  , pathSplittingFeature
  , executeCrucibleDFSPaths
  ) where

import           Control.Lens ( (^.) )
import           Control.Monad.Reader
import           Data.IORef
import           Data.Sequence( Seq )
import qualified Data.Sequence as Seq
import           Data.Word

import           What4.Interface
import           What4.ProgramLoc

import           Lang.Crucible.Backend
import           Lang.Crucible.CFG.Extension
import           Lang.Crucible.Simulator.ExecutionTree
import           Lang.Crucible.Simulator.EvalStmt
import           Lang.Crucible.Simulator.Operations


-- | A `WorkItem` represents a suspended symbolic execution path that
--   can later be resumed.  It captures all the relevant context that
--   is required to recreate the simulator state at the point when
--   the path was suspended.
data WorkItem p sym ext rtp =
  forall f args.
  WorkItem
  { -- | The predicate we branched on to generate this work item
    workItemPred  :: Pred sym
    -- | The location of the symbolic branch
  , workItemLoc   :: ProgramLoc
    -- | The paused execution frame
  , workItemFrame :: PausedFrame p sym ext rtp f
    -- | The overall execution state of this path
  , workItemState :: SimState p sym ext rtp f ('Just args)
    -- | The assumption state of the symbolic backend when we suspended this work item
  , workItemAssumes :: AssumptionState sym
  }

-- | A `WorkList` represents a sequence of `WorkItems` that still
--   need to be explored.
type WorkList p sym ext rtp = IORef (Seq (WorkItem p sym ext rtp))

-- | Put a work item onto the front of the work list.
queueWorkItem :: WorkItem p sym ext rtp -> WorkList p sym ext rtp -> IO ()
queueWorkItem i wl = atomicModifyIORef' wl (\xs -> (i Seq.<| xs, ()))

-- | Pull a work item off the front of the work list, if there are any left.
--   When used with `queueWorkItem`, this function uses the work list as a stack
--   and will explore paths in a depth-first manner.
dequeueWorkItem :: WorkList p sym ext rtp -> IO (Maybe (WorkItem p sym ext rtp))
dequeueWorkItem wl =
  atomicModifyIORef' wl $ \xs ->
     case Seq.viewl xs of
       Seq.EmptyL   -> (xs,  Nothing)
       i Seq.:< xs' -> (xs', Just i)

-- | Given a work item, restore the simulator state so that it is ready to resume
--   exploring the path that it represents.
restoreWorkItem ::
  IsSymInterface sym =>
  WorkItem p sym ext rtp ->
  IO (ExecState p sym ext rtp)
restoreWorkItem (WorkItem branchPred loc frm st assumes) =
  do let sym = st ^. stateSymInterface
     let simCtx = st ^. stateContext
     withBackend simCtx $ \bak ->
      do setCurrentProgramLoc sym loc
         restoreAssumptionState bak assumes
         addAssumption bak (BranchCondition loc (pausedLoc frm) branchPred)
         let ctx = st ^. stateTree . actContext
         runReaderT (resumeFrame frm ctx) st

-- | The path splitting execution feature always selects the \"true\" branch
--   of a symbolic branch to explore first, and pushes the \"false\" branch
--   onto the front of the given work list.  With this feature enabled,
--   a single path will be explored with no symbolic branching until it is finished,
--   and all remaining unexplored paths will be suspended in the work list, where
--   they can be later resumed.
pathSplittingFeature ::
  IsSymInterface sym =>
  WorkList p sym ext rtp ->
  ExecutionFeature p sym ext rtp
pathSplittingFeature wl = ExecutionFeature $ \case
  SymbolicBranchState p trueFrame falseFrame _bt st ->
    withBackend (st^.stateContext) $ \bak ->
    do let sym = st ^. stateSymInterface
       pnot <- notPred sym p
       assumes <- saveAssumptionState bak
       loc <- getCurrentProgramLoc sym

       let wi = WorkItem
                { workItemPred  = pnot
                , workItemLoc   = loc
                , workItemFrame = forgetPostdomFrame falseFrame
                , workItemState = st
                , workItemAssumes = assumes
                }
       queueWorkItem wi wl

       addAssumption bak (BranchCondition loc (pausedLoc trueFrame) p)

       let ctx = st ^. stateTree . actContext
       ExecutionFeatureNewState <$> runReaderT (resumeFrame (forgetPostdomFrame trueFrame) ctx) st

  _ -> return ExecutionFeatureNoChange


-- | This function executes a state using the path splitting execution
--   feature.  Each time a path is completed, the given result
--   continuation is executed on it. If the continuation returns
--   'True', additional paths will be executed; otherwise, we exit early
--   and exploration stops.
--
--   If exploration continues, the next work item will be
--   popped of the front of the work list and will be executed in turn.
--   If a timeout result is encountered, we instead stop executing paths early.
--   The return value of this function is the number of paths that were
--   completed, and a list of remaining paths (if any) that were not
--   explored due to timeout or early exit.
executeCrucibleDFSPaths :: forall p sym ext rtp.
  ( IsSymInterface sym
  , IsSyntaxExtension ext
  ) =>
  [ ExecutionFeature p sym ext rtp ] {- ^ Execution features to install -} ->
  ExecState p sym ext rtp   {- ^ Execution state to begin executing -} ->
  (ExecResult p sym ext rtp -> IO Bool)
    {- ^ Path result continuation, return 'True' to explore more paths -} ->
  IO (Word64, Seq (WorkItem p sym ext rtp))
executeCrucibleDFSPaths execFeatures exst0 cont =
  do wl <- newIORef Seq.empty
     cnt <- newIORef (1::Word64)
     let feats = execFeatures ++ [pathSplittingFeature wl]
     go wl cnt feats exst0

 where
 go wl cnt feats exst =
   do res <- executeCrucible feats exst
      goOn <- cont res
      case res of
        TimeoutResult _ ->
           do xs <- readIORef wl
              i  <- readIORef cnt
              return (i,xs)

        _ | not goOn ->
           do xs <- readIORef wl
              i  <- readIORef cnt
              return (i,xs)

          | otherwise ->
             dequeueWorkItem wl >>= \case
               Nothing ->
                 do i <- readIORef cnt
                    return (i, mempty)

               Just wi ->
                 do modifyIORef' cnt succ
                    restoreWorkItem wi >>= go wl cnt feats