packages feed

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

-----------------------------------------------------------------------
-- |
-- Module           : Lang.Crucible.Simulator.BoundedExec
-- Description      : Support for bounding loop depth
-- Copyright        : (c) Galois, Inc 2018
-- License          : BSD3
-- Maintainer       : Rob Dockins <rdockins@galois.com>
-- Stability        : provisional
--
-- This module provides an execution feature for bounding the
-- number of iterations that a loop will execute in the simulator.
------------------------------------------------------------------------
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

{-# OPTIONS_GHC -Wno-orphans #-}
module Lang.Crucible.Simulator.BoundedExec
  ( boundedExecFeature
  ) where

import           Control.Lens ( (^.), to, (&), (%~), (.~) )
import           Control.Monad ( when )
import           Data.IORef
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Maybe (fromMaybe)
import           Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import           Data.Word


import qualified Data.Parameterized.Context as Ctx
import qualified Data.Parameterized.Map as MapF

import           Lang.Crucible.Analysis.Fixpoint.Components
import           Lang.Crucible.Backend
import           Lang.Crucible.CFG.Core
import           Lang.Crucible.FunctionHandle
import           Lang.Crucible.Panic
import           Lang.Crucible.Simulator.CallFrame
import           Lang.Crucible.Simulator.ExecutionTree
import           Lang.Crucible.Simulator.GlobalState
import           Lang.Crucible.Simulator.Intrinsics
import           Lang.Crucible.Simulator.EvalStmt
import           Lang.Crucible.Simulator.SimError

import           What4.FunctionName
import           What4.Interface

data FrameBoundData =
  forall args ret.
    FrameBoundData
    { frameBoundHandle :: !(FnHandle args ret)
    , frameBoundLimit :: !Word64
    , frameWtoMap :: !(Map Int (Int,Int))
    , frameBoundCounts :: Seq Word64
    }

-- | This function takes weak topological order data and computes
--   a mapping from block ID number to (position, depth) pair.  The
--   position value indicates which the position in the WTO listing
--   in which the block ID appears, and the depth indicates the number
--   of nested components the block ID appears in.  Loop backedges
--   occur exactly at places where control flows from a higher position
--   number to a lower position number.  Jumps that exit inner loops
--   to the next iteration of an outer loop are identified by backedges
--   that pass from higher depths to lower depths.
buildWTOMap :: [WTOComponent (Some (BlockID blocks))] -> Map Int (Int,Int)
buildWTOMap = snd . go 0 0 Map.empty
 where
 go :: Int -> Int -> Map Int (Int,Int) -> [WTOComponent (Some (BlockID blocks))] -> (Int, Map Int (Int,Int))
 go !x !_ m [] = (x,m)
 go !x !d m (Vertex (Some bid) : cs) =
    let m' = Map.insert (Ctx.indexVal (blockIDIndex bid)) (x,d) m
     in go (x+1) d m' cs
 go !x !d m (SCC scc : cs) =
    let m'  = viewSome (\hd -> Map.insert (Ctx.indexVal (blockIDIndex hd)) (x,d+1) m) (wtoHead scc)
        (x',m'') = go (x+1) (d+1) m' $ wtoComps scc
     in go x' d m'' cs


-- | This function updates the loop bound count at the given depth.
--   Any loop bounds deeper than this are discarded.  If the given
--   sequence is too short to accommodate the given depth, the sequence
--   is extended with 0 counters to the correct depth.
incrementBoundCount :: Seq Word64 -> Int -> (Seq Word64, Word64)
incrementBoundCount cs depth =
  case Seq.lookup depth cs of
     Just n ->
       do let n' = n+1
          let cs' = Seq.update depth n' $ Seq.take (depth+1) cs
          n' `seq` cs' `seq` (cs', n')
     Nothing ->
       do let cs' = cs <> Seq.replicate (depth - Seq.length cs) 0 <> Seq.singleton 1
          cs' `seq` (cs', 1)

instance IntrinsicClass sym "BoundedExecFrameData" where
  type Intrinsic sym "BoundedExecFrameData" ctx = [Either FunctionName FrameBoundData]

  muxIntrinsic _sym _iTypes _nm _ _p fd1 fd2 = combineFrameBoundData fd1 fd2

mergeCounts :: Seq Word64 -> Seq Word64 -> Seq Word64
mergeCounts cx cy =
  Seq.fromFunction
    (max (Seq.length cx) (Seq.length cy))
    (\i -> max (fromMaybe 0 $ Seq.lookup i cx)
               (fromMaybe 0 $ Seq.lookup i cy))

mergeFBD ::
  FrameBoundData ->
  FrameBoundData ->
  IO FrameBoundData
mergeFBD x@FrameBoundData{ frameBoundHandle = hx } y@FrameBoundData{ frameBoundHandle = hy }
  | Just _ <- testEquality (handleID hx) (handleID hy) =
       return x{ frameBoundCounts = mergeCounts (frameBoundCounts x) (frameBoundCounts y) }

  | otherwise =
       panic "BoundedExec.mergeFBD"
       [ "Attempted to merge frame bound data from different function activations: "
       , " ** " ++ show hx
       , " ** " ++ show hy
       ]


combineFrameBoundData ::
  [Either FunctionName FrameBoundData] ->
  [Either FunctionName FrameBoundData] ->
  IO [Either FunctionName FrameBoundData]
combineFrameBoundData [] [] = return []

combineFrameBoundData (Left nmx:xs) (Left nmy : _) | nmx == nmy
  = return (Left nmx : xs)

combineFrameBoundData (Right x:xs) (Right y:_)
  = (\x' -> Right x' : xs) <$> mergeFBD x y

combineFrameBoundData xs ys
  = panic "BoundedExec.combineFrameBoundData"
      [ "Attempt to combine incompatible frame bound data: stack shape mismatch:"
      , " *** " ++ show (printStack xs)
      , " *** " ++ show (printStack ys)
      ]

printStack :: [Either FunctionName FrameBoundData] -> [String]
printStack [] = []
printStack (Left nm :xs) = show nm : printStack xs
printStack (Right FrameBoundData{ frameBoundHandle = h } : xs) = show h : printStack xs


type BoundedExecGlobal = GlobalVar (IntrinsicType "BoundedExecFrameData" EmptyCtx)


-- | This execution feature allows users to place a bound on the number
--   of iterations that a loop will execute.  Each time a function is called,
--   the included action is called to determine if the loops in that function
--   should be bounded, and what their iteration bound should be.
--
--   The boolean argument indicates if we should generate proof obligations when
--   we cut off loop execution.  If true, loop cutoffs will generate proof obligations
--   which will be provable only if the loop actually could not have executed that number
--   of iterations.  If false, the execution of loops will be aborted without generating
--   side conditions.
--
--   Note that we compute a weak topological ordering on control flow graphs
--   to determine loop heads and loop nesting structure.  Loop bounds for inner
--   loops are reset on every iteration through an outer loop.
boundedExecFeature ::
  (SomeHandle -> IO (Maybe Word64))
    {- ^ Action for computing loop bounds for functions when they are called -} ->
  Bool {- ^ Produce a proof obligation when resources are exhausted? -} ->
  IO (GenericExecutionFeature sym)
boundedExecFeature getLoopBounds generateSideConditions =
  do gvRef <- newIORef (error "Global variable for BoundedExecFrameData not initialized")
     return $ GenericExecutionFeature $ onStep gvRef

 where
 buildFrameData :: ResolvedCall p sym ext ret -> IO (Either FunctionName FrameBoundData)
 buildFrameData (OverrideCall ov _) = return (Left (overrideName ov))
 buildFrameData (CrucibleCall _entry CallFrame{ _frameCFG = g }) =
   do let wtoMap = buildWTOMap (cfgWeakTopologicalOrdering g)
      mn <- getLoopBounds (SomeHandle (cfgHandle g))
      case mn of
        Nothing -> return $ Left  $ handleName (cfgHandle g)
        Just n  -> return $ Right $ FrameBoundData
                       { frameBoundHandle = cfgHandle g
                       , frameBoundLimit  = n
                       , frameWtoMap      = wtoMap
                       , frameBoundCounts = mempty
                       }

 checkBackedge ::
   IORef BoundedExecGlobal ->
   Some (BlockID blocks) ->
   BlockID blocks tgt_args ->
   SymGlobalState sym ->
   IO (SymGlobalState sym, Maybe Word64)
 checkBackedge gvRef (Some bid_curr) bid_tgt globals =
   do gv <- readIORef gvRef
      case fromMaybe [] (lookupGlobal gv globals) of
        ( Right fbd : rest ) ->
          do let id_curr = Ctx.indexVal (blockIDIndex bid_curr)
             let id_tgt  = Ctx.indexVal (blockIDIndex bid_tgt)
             let m = frameWtoMap fbd
             case (Map.lookup id_curr m, Map.lookup id_tgt m) of
               (Just (cx, _cd), Just (tx, td)) | tx <= cx ->
                  do let cs       = frameBoundCounts fbd
                     let (cs', q) = incrementBoundCount cs td
                     let fbd'     = fbd{ frameBoundCounts = cs' }
                     let globals' = insertGlobal gv (Right fbd' : rest) globals
                     if q > frameBoundLimit fbd then
                       return (globals', Just (frameBoundLimit fbd))
                     else
                       return (globals', Nothing)

               _ -> return (globals, Nothing)
        _ -> return (globals, Nothing)

 modifyStackState ::
   IORef BoundedExecGlobal ->
   (SimState p sym ext rtp f args -> ExecState p sym ext rtp) ->
   SimState p sym ext rtp f args ->
   ([Either FunctionName FrameBoundData] -> [Either FunctionName FrameBoundData]) ->
   IO (ExecutionFeatureResult p sym ext rtp)
 modifyStackState gvRef mkSt st f =
   do gv <- readIORef gvRef
      let xs = case lookupGlobal gv (st ^. stateGlobals) of
                 Nothing -> error "bounded execution global not defined!"
                 Just v  -> v
      let st' = st & stateGlobals %~ insertGlobal gv (f xs)
      return (ExecutionFeatureModifiedState (mkSt st'))

 onTransition ::
   IORef BoundedExecGlobal ->
   BlockID blocks tgt_args ->
   ControlResumption p sym ext rtp (CrucibleLang blocks ret) ->
   SimState p sym ext rtp (CrucibleLang blocks ret) ('Just a) ->
   IO (ExecutionFeatureResult p sym ext rtp)
 onTransition gvRef tgt_id res st = stateSolverProof st $
  do let sym = st^.stateSymInterface
     let simCtx = st^.stateContext
     (globals', overLimit) <- checkBackedge gvRef (st^.stateCrucibleFrame.frameBlockID) tgt_id (st^.stateGlobals)
     let st' = st & stateGlobals .~ globals'
     case overLimit of
       Just n ->
         do let msg = "reached maximum number of loop iterations (" ++ show n ++ ")"
            let loc = st^.stateCrucibleFrame.to frameProgramLoc
            let err = SimError loc (ResourceExhausted msg)
            when generateSideConditions $ withBackend simCtx $ \bak ->
              addProofObligation bak (LabeledPred (falsePred sym) err)
            return (ExecutionFeatureNewState (AbortState (AssertionFailure err) st'))
       Nothing -> return (ExecutionFeatureModifiedState (ControlTransferState res st'))

 onStep ::
   IORef BoundedExecGlobal ->
   ExecState p sym ext rtp ->
   IO (ExecutionFeatureResult p sym ext rtp)

 onStep gvRef = \case
   InitialState simctx globals ah ret cont ->
     do let halloc = simHandleAllocator simctx
        gv <- freshGlobalVar halloc (Text.pack "BoundedExecFrameData") knownRepr
        writeIORef gvRef gv
        let globals' = insertGlobal gv [Left "_init"] globals
        let simctx' = simctx{ ctxIntrinsicTypes = MapF.insert (knownSymbol @"BoundedExecFrameData") IntrinsicMuxFn (ctxIntrinsicTypes simctx) }
        return (ExecutionFeatureModifiedState (InitialState simctx' globals' ah ret cont))

   CallState rh call st ->
     do boundData <- buildFrameData call
        modifyStackState gvRef (CallState rh call) st (boundData:)

   TailCallState vfv call st ->
     do boundData <- buildFrameData call
        modifyStackState gvRef (TailCallState vfv call) st ((boundData:) . drop 1)

   ReturnState nm vfv pr st ->
        modifyStackState gvRef (ReturnState nm vfv pr) st (drop 1)

   UnwindCallState vfv ar st ->
        modifyStackState gvRef (UnwindCallState vfv ar) st (drop 1)

   ControlTransferState res st ->
     case res of
       ContinueResumption (ResolvedJump tgt_id _)  ->  onTransition gvRef tgt_id res st
       CheckMergeResumption (ResolvedJump tgt_id _) -> onTransition gvRef tgt_id res st
       _ -> return ExecutionFeatureNoChange

   _ -> return ExecutionFeatureNoChange