sbv-program-1.0.0.0: src/Data/SBV/Program.hs
--
-- | This module implements an algorithm described in the
-- "Component-based Synthesis Applied to Bitvector Programs" paper.
--
-- https://www.microsoft.com/en-us/research/wp-content/uploads/2010/02/bv.pdf
--
-- Given a program specification along with a library of available components
-- it synthesizes an actual program using an off-the-shelf SMT-solver.
--
-- The specification is an arbitrary datatype that is an instance of the 'SynthSpec' class.
-- The library is a list of 'SynthComponent' instances.
--
-- There are three entry points to this module: 'standardExAllProcedure', 'refinedExAllProcedure' and 'exAllProcedure'.
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RecordWildCards #-}
module Data.SBV.Program (
-- * Definitions for specification and component classes and the resulting data type
module Data.SBV.Program.Types,
-- * Predefined components library
module Data.SBV.Program.SimpleLibrary,
-- * Various utility functions
module Data.SBV.Program.Utils,
-- * Package entry points #entry#
standardExAllProcedure,
refinedExAllProcedure,
exAllProcedure,
-- * Auxiliary functions that make up synthesis procedures #aux#
createProgramLocs,
constrainLocs,
createProgramVarsWith,
createVarsConstraints,
)
where
import Control.Monad
import Control.Monad.IO.Class
import Data.Either
import Data.Foldable
import Data.List
import Data.Maybe
import Data.Ord (comparing)
import Data.Traversable (for)
import Data.SBV hiding (STuple)
import Data.SBV.Control
import Data.SBV.Program.SimpleLibrary
import Data.SBV.Program.Types
import Data.SBV.Program.Utils
import Text.Pretty.Simple (pPrint)
-- | Represents a failed run in 'standardExAllProcedure'. Corresponds to
-- __program variable__ \(S\) in the paper.
data STuple a = STuple {
s_ios :: IOs a,
s_comps :: [IOs a]
}
deriving Show
-- | An implementation of __StandardExAllSolver__ presented in section 6.1 of the paper.
-- As stated in the paper, this implementation boils down to exhaustive enumeration
-- of possible solutions, and as such isn't effective. It can be used to better
-- understand how the synthesis procedure works and provides a lot of debugging
-- output. Do not use this procedure for solving real problems.
standardExAllProcedure :: forall a comp spec .
(SymVal a, Show a, SynthSpec spec a, SynthComponent comp spec a) =>
-- | Component library
[comp a]
-- | Specification of program being synthesized
-> spec a
-> IO (Either SynthesisError (Program Location (comp a)))
standardExAllProcedure library spec = do
-- generate a seed for S
mbRes <- sampleSpec spec
case mbRes of
Nothing -> return $ Left ErrorSeedingFailed
Just i0 -> do
putStrLn "Seeding done, result:"
print i0
go 1 [STuple (i0 :: IOs a) []]
where
n = genericLength library
numInputs = specArity spec
m = n + numInputs
go step s = do
putStrLn "============="
putStrLn "Synthesizing with s = "
pPrint s
-- Finite synthesis part
r <- runSMT $ do
progLocs <- createProgramLocs library numInputs
constrainLocs m numInputs progLocs
-- Unlike 'exAllProcedure' we call 'createProgramVarsWith' here multiple times
-- Since we aren't using forall quantifier, we have to create variables
-- for each STuple item in s.
manyProgVars <- forM s $ \(STuple {..}) -> do
let numInputs = genericLength $ _ins s_ios
progVars <- createProgramVarsWith sbvExists library numInputs
-- pin input/output variables (members of I and O sets) to values from S
constrain $ fmap literal s_ios .== programIOs progVars
-- on the first run we don't have values for component locations (members of T set in the paper)
unless (null s_comps) $
constrain $ map (fmap literal) s_comps .== map instructionIOs (programInstructions progVars)
return progVars
forM_ manyProgVars $ \progVars -> do
let (IOs inputVars outputVar) = programIOs progVars
(psi_conn, phi_lib) = createVarsConstraints progLocs progVars
constrain $ (phi_lib .&& psi_conn) .=> specFunc spec inputVars outputVar
query $ do
r <- checkSat
case r of
Sat -> do
solution <- mapM getValue (programIOs progLocs)
componentLocVals <- traverse (bimapM getValue pure) (programInstructions progLocs)
return $ Right (Program solution componentLocVals)
Unsat -> return $ Left ErrorUnsat
Unk -> Left . ErrorUnknown . show <$> getUnknownReason
-- Verification part
-- At this stage the 'currL' program represents a solution that is known to
-- work for all values from S. We now check if this solution works for all
-- values possible.
fmap join $ for r $ \currL -> runSMT $ do
liftIO $ do
putStrLn "Synthesis step done, current solution:"
putStrLn $ writePseudocode currL
progLocs <- createProgramLocs library numInputs
-- In the verification part we pin location variables L
constrain $ (literal <$> programIOs currL) .== programIOs progLocs
constrain $ sAnd $ zipWith (\x y -> literal x .== y) (concatMap (toList .instructionIOs) (programInstructions currL)) (concatMap (toList .instructionIOs) (programInstructions progLocs))
progVars <- createProgramVarsWith sbvExists library numInputs
let Program (IOs inputVars outputVar) componentVars = progVars
(psi_conn, phi_lib) = createVarsConstraints progLocs progVars
constrain $ sNot $ (phi_lib .&& psi_conn) .=> specFunc spec inputVars outputVar
query $ do
r <- checkSat
io $ putStrLn "Verification step done"
case r of
Unsat -> do
io $ do
putStrLn "============="
putStrLn ("Solution found after " ++ show step ++ " iterations")
return $ Right currL
Sat -> do
io $ putStrLn "Solution does not work for all inputs"
inputVals <- mapM getValue inputVars
outputVal <- getValue outputVar
componentVals <- mapM (traverse getValue . instructionIOs) componentVars
io $ go (step + 1) $ STuple (IOs inputVals outputVal) componentVals : s
-- | An implementation of __RefinedExAllSolver__ presented in section 6.2 of the paper.
-- This is an improved version of 'standardExAllProcedure'. It only keeps input
-- values \(|\vec I|\) in \(S\) and uses different synthesis constraints on __synthesis__
-- and __verification__ steps.
refinedExAllProcedure :: forall a comp spec .
(SymVal a, SynthSpec spec a, SynthComponent comp spec a) =>
-- | Component library
[comp a]
-- | Specification of program being synthesized
-> spec a
-> IO (Either SynthesisError (Program Location (comp a)))
refinedExAllProcedure library spec = do
mbRes <- sampleSpec spec
case mbRes of
Nothing -> return $ Left ErrorSeedingFailed
Just r -> go 1 ([_ins r] :: [[a]])
where
n = genericLength library
numInputs = specArity spec
m = n + numInputs
go step s = do
-- Finite synthesis part
r <- runSMT $ do
progLocs <- createProgramLocs library numInputs
constrainLocs m numInputs progLocs
-- Unlike 'exAllProcedure' here we call 'createProgramVarsWith' multiple times
-- Since we aren't using forall quantifier, we have to create variables
-- for each I vector in s.
manyProgVars <- forM s $ \inputVars_s -> do
let numInputs = genericLength inputVars_s
progVars <- createProgramVarsWith sbvExists library numInputs
-- pin input variables (members of I set) to values from S
constrain $ fmap literal inputVars_s .== _ins (programIOs progVars)
return progVars
forM_ manyProgVars $ \progVars -> do
let (IOs inputVars outputVar) = programIOs progVars
(psi_conn, phi_lib) = createVarsConstraints progLocs progVars
constrain $ (phi_lib .&& psi_conn) .&& specFunc spec inputVars outputVar
query $ do
r <- checkSat
case r of
Sat -> Right <$> bitraverse getValue pure progLocs
_ -> return $ Left ErrorUnsat
-- Verification part
-- At this stage the 'currL' program represents a solution that is known to
-- work for all values from S. We now check if this solution works for all
-- values possible.
fmap join $ for r $ \currL -> runSMT $ do
progLocs <- createProgramLocs library numInputs
-- In the verification part we pin location variables L
constrain $ (literal <$> programIOs currL) .== programIOs progLocs
constrain $ sAnd $ zipWith (\x y -> literal x .== y) (concatMap (toList .instructionIOs) (programInstructions currL)) (concatMap (toList .instructionIOs) (programInstructions progLocs))
progVars <- createProgramVarsWith sbvExists library numInputs
let (Program (IOs inputVars outputVar) componentVars) = progVars
(psi_conn, phi_lib) = createVarsConstraints progLocs progVars
constrain $ (phi_lib .&& psi_conn) .&& sNot (specFunc spec inputVars outputVar)
query $ do
r <- checkSat
case r of
Unsat -> return $ Right currL
Sat -> do
inputVals <- mapM getValue inputVars
outputVal <- getValue outputVar
componentVals <- mapM (traverse getValue . instructionIOs) componentVars
io $ go (step + 1) $ inputVals : s
-- | This procedure is not part of the paper. It uses forall quantification directly
-- when creating variables from the \(T\) set. As consequence it requires an SMT-solver
-- than can handle foralls (for instance, Z3). This procedure is the easiest to
-- understand.
exAllProcedure :: forall a comp spec .
(SymVal a, SynthSpec spec a, SynthComponent comp spec a) =>
-- | Component library
[comp a]
-- | Specification of program being synthesized
-> spec a
-> IO (Either SynthesisError (Program Location (comp a)))
exAllProcedure library spec =
-- run SBV with 'allowQuantifiedQueries' to silence warnings about entering
-- 'query' mode in presence of universally quantified variables
runSMTWith (defaultSMTCfg {allowQuantifiedQueries = True}) $ do
let n = genericLength library
numInputs = specArity spec
m = n + numInputs
progLocs <- createProgramLocs library numInputs
constrainLocs m numInputs progLocs
progVars <- createProgramVarsWith sbvForall library numInputs
let (Program (IOs inputVars outputVar) _) = progVars
(psi_conn, phi_lib) = createVarsConstraints progLocs progVars
constrain $ (phi_lib .&& psi_conn) .=> specFunc spec inputVars outputVar
query $ do
r <- checkSat
case r of
Sat -> do
inputLocVals <- mapM getValue (_ins $ programIOs progLocs)
outputLocVal <- getValue (_out $ programIOs progLocs)
componentLocVals <- traverse (bimapM getValue pure) (programInstructions progLocs)
return $ Right $ Program (IOs inputLocVals outputLocVal) (sortOn (_out . instructionIOs) componentLocVals)
Unsat -> return $ Left ErrorUnsat
Unk -> do
reason <- getUnknownReason
return $ Left $ ErrorUnknown $ "Unknown: " ++ show reason
-- | First step of each synthesis procedure. Given a library of components and
-- a number of program's inputs, it creates existentially quantified
-- __location variables__ (members of set \(L\) in the paper) for each component
-- and for the program itself.
createProgramLocs :: forall a comp spec . (SymVal a, SynthSpec spec a, SynthComponent comp spec a) => [comp a] -> Word -> Symbolic (Program SLocation (comp a))
createProgramLocs library numInputs = do
inputLocs <- mapM sbvExists $ genericTake numInputs ["InputLoc" ++ show i | i <- [1..]] :: Symbolic [SLocation]
outputLoc <- sbvExists "OutputLoc" :: Symbolic SLocation
componentsWithLocs <- forM library $ \component -> do
let n' = specArity $ compSpec component
compInputLocs <- mapM sbvExists $ genericTake n' [mkInputLocName (compName component) i | i <- [1..]]
compOutputLoc <- sbvExists $ mkOutputLocName $ compName component
return $ Instruction (IOs compInputLocs compOutputLoc) component :: Symbolic (Instruction SLocation (comp a))
return $ Program (IOs inputLocs outputLoc) componentsWithLocs
-- | Second step of each synthesis procedure. It applies constraints on
-- __location variables__ from section 5 of the original paper. These constraints
-- include __well-formedness constraint__ \(ψ_{wfp}\), __acyclicity constraint__
-- \(ψ_{acyc}\) and __consistency constraint__ \(ψ_{cons}\). Constraints are not
-- returned from this function, but are applied immediately. Section 5 of the
-- paper also talks about __connectivity constraint__ \(ψ_{conn}\), which is
-- not created here.
constrainLocs :: (SynthSpec spec a, SynthComponent comp spec a) =>
-- | The \(M\) constant from the paper, which equals to \(N + |\vec I|\), where
-- __N__ is the size of the library.
Word
-- | Number of program inputs \(|\vec I|\).
-> Word
-> Program SLocation (comp a) -> Symbolic ()
constrainLocs m numInputs (Program {..}) = do
-- program inputs are assigned locations from 0 to numInputs
forM_ (zip [0..] (_ins programIOs)) $ \(i, inputLoc) -> do
constrain $ inputLoc .== literal i
-- program output location should not be greater than the number of instructions
constrain $ _out programIOs .< fromIntegral m
forM_ programInstructions $ \(Instruction (IOs compInputLocs compOutputLoc) comp) -> do
forM_ compInputLocs $ \inputLoc -> do
-- psi_wfp for component inputs
constrain $ inputLoc .>= literal 0
constrain $ inputLoc .<= literal (fromIntegral $ m-1)
-- psi_acyc
constrain $ inputLoc .< compOutputLoc
-- psi_wfp for component outputs
constrain $ compOutputLoc .>= literal (fromIntegral numInputs)
constrain $ compOutputLoc .<= literal (fromIntegral $ m-1)
-- extra constraints supplied by user
forM_ (extraLocConstrs comp) $ \extraConstr ->
constrain $ extraConstr compInputLocs compOutputLoc
-- psi_cons
constrain $ distinct $ map (_out . instructionIOs) programInstructions
-- | Third step of the synthesis process. It creates variables that represent
-- actual inputs/outputs values (members of the set \(T\) in the paper). This
-- function resembles 'createProgramLocs', but unlike it allows creating both existentially
-- and universally quantified variables. Standard and Refined procedures pass
-- 'sbvExists' to create existentially quantified variables, while 'exAllProcedure'
-- uses 'sbvForall'.
createProgramVarsWith :: forall a comp spec . (SymVal a, SynthSpec spec a, SynthComponent comp spec a) =>
-- | Variable creation function. Either 'sbvExists' or 'sbvForall'.
(String -> Symbolic (SBV a))
-- | Component library.
-> [comp a]
-- | Number of program inputs \(|\vec I|\).
-> Word
-> Symbolic (Program (SBV a) (comp a))
createProgramVarsWith sbvMakeFunc library numInputs = do
inputVars <- mapM sbvMakeFunc $ genericTake numInputs ["Input" ++ show i | i <- [1..]]
outputVar <- sbvMakeFunc "Output"
componentVars <- forM library $ \comp -> do
let n' = specArity $ compSpec comp
compInputVars <- mapM sbvMakeFunc $ genericTake n' [mkInputVarName (compName comp) i | i <- [1..]]
compOutputVar <- sbvMakeFunc $ mkOutputVarName $ compName comp
return $ Instruction (IOs compInputVars compOutputVar) comp
return $ Program (IOs inputVars outputVar) componentVars
-- | Last building block of the synthesis process. This function creates
-- \(ψ_{conn}\) and \(φ_{lib}\) constraints and return them.
createVarsConstraints :: SynthComponent comp spec a => Program SLocation (comp a) -> Program (SBV a) (comp a) -> (SBool, SBool)
createVarsConstraints progLocs progVars = (psi_conn, phi_lib)
where
allVarsWithLocs = zip (toIOsList progVars) (toIOsList progLocs)
psi_conn = sAnd [(xLoc .== yLoc) .=> (x .== y) | (x, xLoc) <- allVarsWithLocs, (y, yLoc) <- allVarsWithLocs]
phi_lib = sAnd $ flip map (programInstructions progVars) $
\(Instruction (IOs inputVars outputVar) comp) -> specFunc (compSpec comp) inputVars outputVar