grisette-0.4.1.0: src/Grisette/Core/Data/Class/CEGISSolver.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE Trustworthy #-}
-- |
-- Module : Grisette.Core.Data.Class.CEGISSolver
-- Copyright : (c) Sirui Lu 2021-2023
-- License : BSD-3-Clause (see the LICENSE file)
--
-- Maintainer : siruilu@cs.washington.edu
-- Stability : Experimental
-- Portability : GHC only
module Grisette.Core.Data.Class.CEGISSolver
( -- * Note for the examples
--
-- | The examples assumes that the [z3](https://github.com/Z3Prover/z3)
-- solver is available in @PATH@.
-- * Generic CEGIS interface
SynthesisConstraintFun,
VerifierResult (..),
StatefulVerifierFun,
CEGISResult (..),
genericCEGIS,
-- * CEGIS interfaces with pre/post conditions
CEGISCondition (..),
cegisPostCond,
cegisPrePost,
cegisMultiInputs,
cegis,
cegisExcept,
cegisExceptStdVC,
cegisExceptVC,
cegisExceptMultiInputs,
cegisExceptStdVCMultiInputs,
cegisExceptVCMultiInputs,
cegisForAll,
cegisForAllExcept,
cegisForAllExceptStdVC,
cegisForAllExceptVC,
)
where
import Control.Monad (foldM, unless)
import Data.List (partition)
import GHC.Generics (Generic)
import Generics.Deriving (Default (Default))
import Grisette.Core.Control.Exception
( VerificationConditions (AssertionViolation, AssumptionViolation),
)
import Grisette.Core.Data.Class.EvaluateSym (EvaluateSym, evaluateSym)
import Grisette.Core.Data.Class.ExtractSymbolics
( ExtractSymbolics,
extractSymbolics,
)
import Grisette.Core.Data.Class.LogicalOp (LogicalOp (symNot, (.&&)))
import Grisette.Core.Data.Class.Mergeable (Mergeable)
import Grisette.Core.Data.Class.ModelOps
( ModelOps (exact, exceptFor),
SymbolSetOps (isEmptySet),
)
import Grisette.Core.Data.Class.SEq (SEq)
import Grisette.Core.Data.Class.SimpleMergeable
( SimpleMergeable,
UnionPrjOp,
simpleMerge,
)
import Grisette.Core.Data.Class.Solvable (Solvable (con))
import Grisette.Core.Data.Class.Solver
( ConfigurableSolver,
Solver (solverSolve),
SolvingFailure (Unsat),
UnionWithExcept (extractUnionExcept),
solve,
withSolver,
)
import Grisette.IR.SymPrim.Data.Prim.Model (Model)
import Grisette.IR.SymPrim.Data.SymPrim (SymBool)
-- $setup
-- >>> import Grisette.Core
-- >>> import Grisette.Lib.Base
-- >>> import Grisette.IR.SymPrim
-- >>> import Grisette.Backend.SBV
-- | Synthesis constraint function.
--
-- The first argument is the iteration number, for angelic programs, you can use
-- it to instantiate the angelic variables in the program.
--
-- The second argument is the counter-example generated by the verifier.
--
-- The synthesizer will try to find a program that is true for all the synthesis
-- constraints.
type SynthesisConstraintFun input = Int -> input -> IO SymBool
-- | The result of the verifier.
data VerifierResult input exception
= CEGISVerifierFoundCex input
| CEGISVerifierNoCex
| CEGISVerifierException exception
-- | The verifier function.
--
-- The first argument is the state of the verifier.
--
-- The second argument is the candidate model proposed by the synthesizer.
type StatefulVerifierFun state input exception =
state -> Model -> IO (state, VerifierResult input exception)
-- | The result of the CEGIS procedure.
data CEGISResult exception
= CEGISSuccess Model
| CEGISVerifierFailure exception
| CEGISSolverFailure SolvingFailure
deriving (Show)
-- | Generic CEGIS procedure.
--
-- The CEGIS procedure will try to find a model that satisfies the initial
-- synthesis constraint, and satisfies all the inputs generated by the verifier.
genericCEGIS ::
(ConfigurableSolver config handle) =>
-- | Configuration of the solver.
config ->
-- | The initial synthesis constraint.
SymBool ->
-- | The synthesis constraint function.
SynthesisConstraintFun input ->
-- | The initial state of the verifier.
verifierState ->
-- | The verifier function.
StatefulVerifierFun verifierState input exception ->
IO ([input], CEGISResult exception)
genericCEGIS config initConstr synthConstr initVerifierState verifier =
withSolver config $ \solver -> do
firstResult <- solverSolve solver initConstr
case firstResult of
Left err -> return ([], CEGISSolverFailure err)
Right model -> go solver model 0 initVerifierState
where
go solver prevModel iterNum verifierState = do
(newVerifierState, verifierResult) <-
verifier verifierState prevModel
case verifierResult of
CEGISVerifierFoundCex cex -> do
newResult <- solverSolve solver =<< synthConstr iterNum cex
case newResult of
Left err -> return ([], CEGISSolverFailure err)
Right model -> do
(cexes, result) <- go solver model (iterNum + 1) newVerifierState
return (cex : cexes, result)
CEGISVerifierNoCex -> return ([], CEGISSuccess prevModel)
CEGISVerifierException exception ->
return ([], CEGISVerifierFailure exception)
data CEGISMultiInputsState input = CEGISMultiInputsState
{ _cegisMultiInputsRemainingSymInputs :: [input],
_cegisMultiInputsPre :: SymBool,
_cegisMultiInputsPost :: SymBool
}
-- | The condition for CEGIS to solve.
--
-- The first argument is the pre-condition, and the second argument is the
-- post-condition.
--
-- The CEGIS procedures would try to find a model for the formula
--
-- \[
-- \forall P. (\exists I. \mathrm{pre}(P, I)) \wedge (\forall I. \mathrm{pre}(P, I)\implies \mathrm{post}(P, I))
-- \]
--
-- In program synthesis tasks, \(P\) is the symbolic constants in the symbolic
-- program, and \(I\) is the input. The pre-condition is used to restrict the
-- search space of the program. The procedure would only return programs that
-- meets the pre-conditions on every possible inputs, and there are at least
-- one possible input. The post-condition is used to specify the desired program
-- behaviors.
data CEGISCondition = CEGISCondition SymBool SymBool
deriving (Generic)
deriving (EvaluateSym) via (Default CEGISCondition)
-- | Construct a CEGIS condition with only a post-condition. The pre-condition
-- would be set to true, meaning that all programs in the program space are
-- allowed.
cegisPostCond :: SymBool -> CEGISCondition
cegisPostCond = CEGISCondition (con True)
-- | Construct a CEGIS condition with both pre- and post-conditions.
cegisPrePost :: SymBool -> SymBool -> CEGISCondition
cegisPrePost = CEGISCondition
deriving via (Default CEGISCondition) instance Mergeable CEGISCondition
deriving via (Default CEGISCondition) instance SimpleMergeable CEGISCondition
-- |
-- CEGIS with multiple (possibly symbolic) inputs. Solves the following formula
-- (see 'CEGISCondition' for details).
--
-- \[
-- \forall P. (\exists I\in\mathrm{inputs}. \mathrm{pre}(P, I)) \wedge (\forall I\in\mathrm{inputs}. \mathrm{pre}(P, I)\implies \mathrm{post}(P, I))
-- \]
--
-- For simpler queries, where the inputs are representable by a single
-- symbolic value, you may want to use 'cegis' or 'cegisExcept' instead.
-- We have an example for the 'cegis' call.
cegisMultiInputs ::
( EvaluateSym input,
ExtractSymbolics input,
ConfigurableSolver config handle
) =>
config ->
[input] ->
(input -> CEGISCondition) ->
IO ([input], CEGISResult SolvingFailure)
cegisMultiInputs config inputs toCEGISCondition = do
initConstr <- cexesAssertFun conInputs
genericCEGIS
config
initConstr
synthConstr
(CEGISMultiInputsState symInputs (con True) (con True))
verifier
where
(conInputs, symInputs) = partition (isEmptySet . extractSymbolics) inputs
forallSymbols = extractSymbolics symInputs
cexAssertFun input = do
unless (isEmptySet (extractSymbolics input)) $ error "BUG"
CEGISCondition pre post <- return $ toCEGISCondition input
return $ pre .&& post
cexesAssertFun = foldM (\acc x -> (acc .&&) <$> cexAssertFun x) (con True)
synthConstr _ = cexAssertFun
verifier state@(CEGISMultiInputsState [] _ _) _ =
return (state, CEGISVerifierNoCex)
verifier
(CEGISMultiInputsState (nextSymInput : symInputs) pre post)
candidate = do
CEGISCondition nextPre nextPost <-
return $ toCEGISCondition nextSymInput
let newPre = pre .&& nextPre
let newPost = post .&& nextPost
let evaluated =
evaluateSym False (exceptFor forallSymbols candidate) $
newPre .&& symNot newPost
r <- solve config evaluated
case r of
Left Unsat ->
verifier (CEGISMultiInputsState symInputs newPre newPost) candidate
Left err ->
return
( CEGISMultiInputsState [] newPre newPost,
CEGISVerifierException err
)
Right model ->
return
( CEGISMultiInputsState (nextSymInput : symInputs) newPre newPost,
CEGISVerifierFoundCex $
evaluateSym False (exact forallSymbols model) nextSymInput
)
-- |
-- CEGIS with a single symbolic input to represent a set of inputs.
--
-- The following example tries to find the value of @c@ such that for all
-- positive @x@, @x * c < 0 && c > -2@. The @c .> -2@ clause is used to make
-- the solution unique.
--
-- >>> :set -XOverloadedStrings
-- >>> let [x,c] = ["x","c"] :: [SymInteger]
-- >>> cegis (precise z3) x (\x -> cegisPrePost (x .> 0) (x * c .< 0 .&& c .> -2))
-- (...,CEGISSuccess (Model {c -> -1 :: Integer}))
cegis ::
( ConfigurableSolver config handle,
EvaluateSym inputs,
ExtractSymbolics inputs,
SEq inputs
) =>
-- | The configuration of the solver
config ->
-- | Initial symbolic inputs. The solver will try to find a
-- program that works on all the inputs representable by it (see
-- 'CEGISCondition').
inputs ->
-- | The condition for the solver to solve. All the
-- symbolic constants that are not in the inputs will
-- be considered as part of the symbolic program.
(inputs -> CEGISCondition) ->
-- | The counter-examples generated
-- during the CEGIS loop, and the
-- model found by the solver.
IO ([inputs], CEGISResult SolvingFailure)
cegis config inputs = cegisMultiInputs config [inputs]
-- |
-- CEGIS for symbolic programs with error handling, using multiple (possibly
-- symbolic) inputs to represent a set of inputs.
cegisExceptMultiInputs ::
( ConfigurableSolver config handle,
EvaluateSym inputs,
ExtractSymbolics inputs,
UnionWithExcept t u e v,
UnionPrjOp u,
Monad u
) =>
config ->
[inputs] ->
(Either e v -> CEGISCondition) ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExceptMultiInputs config cexes interpretFun f =
cegisMultiInputs
config
cexes
(simpleMerge . (interpretFun <$>) . extractUnionExcept . f)
-- |
-- CEGIS for symbolic programs with error handling, using multiple (possibly
-- symbolic) inputs to represent a set of inputs.
--
-- The errors should be translated to assertion or assumption violations.
cegisExceptVCMultiInputs ::
( ConfigurableSolver config handle,
EvaluateSym inputs,
ExtractSymbolics inputs,
UnionWithExcept t u e v,
UnionPrjOp u,
Monad u
) =>
config ->
[inputs] ->
(Either e v -> u (Either VerificationConditions ())) ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExceptVCMultiInputs config cexes interpretFun f =
cegisMultiInputs
config
cexes
( \v ->
simpleMerge
( ( \case
Left AssumptionViolation -> cegisPrePost (con False) (con True)
Left AssertionViolation -> cegisPostCond (con False)
_ -> cegisPostCond (con True)
)
<$> (extractUnionExcept (f v) >>= interpretFun)
)
)
-- |
-- CEGIS for symbolic programs with error handling, using multiple (possibly
-- symbolic) inputs to represent a set of inputs. This function saves the
-- efforts to implement the translation function for the standard error type
-- 'VerificationConditions', and the standard result type '()'.
--
-- This function translates assumption violations to failed pre-conditions,
-- and translates assertion violations to failed post-conditions.
-- The '()' result will not fail any conditions.
cegisExceptStdVCMultiInputs ::
( ConfigurableSolver config handle,
EvaluateSym inputs,
ExtractSymbolics inputs,
UnionWithExcept t u VerificationConditions (),
UnionPrjOp u,
Monad u
) =>
config ->
[inputs] ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExceptStdVCMultiInputs config cexes =
cegisExceptVCMultiInputs config cexes return
-- |
-- CEGIS for symbolic programs with error handling, using a single symbolic
-- input to represent a set of inputs.
--
-- 'cegisExcept' is particularly useful when custom error types are used.
-- With 'cegisExcept', you define how the errors are interpreted to the
-- CEGIS conditions after the symbolic evaluation. This could increase the
-- readability and modularity of the code.
--
-- The following example tries to find the value of @c@ such that for all
-- positive @x@, @x * c < 0 && c > -2@. The @c .> -2@ assertion is used to make
-- the solution unique.
--
-- >>> :set -XOverloadedStrings
-- >>> let [x,c] = ["x","c"] :: [SymInteger]
-- >>> import Control.Monad.Except
-- >>> :{
-- res :: SymInteger -> ExceptT VerificationConditions UnionM ()
-- res x = do
-- symAssume $ x .> 0
-- symAssert $ x * c .< 0
-- symAssert $ c .> -2
-- :}
--
-- >>> :{
-- translation (Left AssumptionViolation) = cegisPrePost (con False) (con True)
-- translation (Left AssertionViolation) = cegisPostCond (con False)
-- translation _ = cegisPostCond (con True)
-- :}
--
-- >>> cegisExcept (precise z3) x translation res
-- ([...],CEGISSuccess (Model {c -> -1 :: Integer}))
cegisExcept ::
( UnionWithExcept t u e v,
UnionPrjOp u,
Functor u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
(Either e v -> CEGISCondition) ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExcept config inputs f v =
cegis config inputs $ \i -> simpleMerge $ f <$> extractUnionExcept (v i)
-- |
-- CEGIS for symbolic programs with error handling, using a single symbolic
-- input to represent a set of inputs.
--
-- The errors should be translated to assertion or assumption violations.
cegisExceptVC ::
( UnionWithExcept t u e v,
UnionPrjOp u,
Monad u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
(Either e v -> u (Either VerificationConditions ())) ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExceptVC config inputs f v = do
cegis config inputs $ \i ->
simpleMerge $
( \case
Left AssumptionViolation -> cegisPrePost (con False) (con True)
Left AssertionViolation -> cegisPostCond (con False)
_ -> cegisPostCond (con True)
)
<$> (extractUnionExcept (v i) >>= f)
-- |
-- CEGIS for symbolic programs with error handling, using a single symbolic
-- input to represent a set of inputs. This function saves the efforts to
-- implement the translation function for the standard error type
-- 'VerificationConditions', and the standard result type '()'.
--
-- This function translates assumption violations to failed pre-conditions,
-- and translates assertion violations to failed post-conditions.
-- The '()' result will not fail any conditions.
--
-- The following example tries to find the value of @c@ such that for all
-- positive @x@, @x * c < 0 && c > -2@. The @c .> -2@ assertion is used to make
-- the solution unique.
--
-- >>> :set -XOverloadedStrings
-- >>> let [x,c] = ["x","c"] :: [SymInteger]
-- >>> import Control.Monad.Except
-- >>> :{
-- res :: SymInteger -> ExceptT VerificationConditions UnionM ()
-- res x = do
-- symAssume $ x .> 0
-- symAssert $ x * c .< 0
-- symAssert $ c .> -2
-- :}
--
-- >>> cegisExceptStdVC (precise z3) x res
-- ([...],CEGISSuccess (Model {c -> -1 :: Integer}))
cegisExceptStdVC ::
( UnionWithExcept t u VerificationConditions (),
UnionPrjOp u,
Monad u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
(inputs -> t) ->
IO ([inputs], CEGISResult SolvingFailure)
cegisExceptStdVC config inputs = cegisExceptVC config inputs return
-- |
-- CEGIS with a single symbolic input to represent a set of inputs.
--
-- The following example tries to find the value of @c@ such that for all
-- positive @x@, @x * c < 0 && c > -2@. The @c .> -2@ clause is used to make
-- the solution unique.
--
-- >>> :set -XOverloadedStrings
-- >>> let [x,c] = ["x","c"] :: [SymInteger]
-- >>> cegisForAll (precise z3) x $ cegisPrePost (x .> 0) (x * c .< 0 .&& c .> -2)
-- (...,CEGISSuccess (Model {c -> -1 :: Integer}))
cegisForAll ::
( ExtractSymbolics forallInput,
ConfigurableSolver config handle
) =>
config ->
-- | A symbolic value. All the symbolic constants in the value are treated as
-- for-all variables.
forallInput ->
CEGISCondition ->
-- | First output are the counter-examples for all the for-all variables, and
-- the second output is the model for all other variables if CEGIS succeeds.
IO ([Model], CEGISResult SolvingFailure)
cegisForAll config input (CEGISCondition pre post) = do
(models, result) <- genericCEGIS config phi synthConstr () verifier
let exactResult = case result of
CEGISSuccess model -> CEGISSuccess $ exceptFor forallSymbols model
_ -> result
return (models, exactResult)
where
phi = pre .&& post
negphi = pre .&& symNot post
forallSymbols = extractSymbolics input
synthConstr _ model = return $ evaluateSym False model phi
verifier () candidate = do
let evaluated =
evaluateSym False (exceptFor forallSymbols candidate) negphi
r <- solve config evaluated
case r of
Left Unsat -> return ((), CEGISVerifierNoCex)
Left err -> return ((), CEGISVerifierException err)
Right model ->
return ((), CEGISVerifierFoundCex $ exact forallSymbols model)
-- |
-- CEGIS for symbolic programs with error handling, with a forall variable.
--
-- See 'cegisForAll' and 'cegisExcept'.
cegisForAllExcept ::
( UnionWithExcept t u e v,
UnionPrjOp u,
Functor u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
(Either e v -> CEGISCondition) ->
t ->
IO ([Model], CEGISResult SolvingFailure)
cegisForAllExcept config inputs f v =
cegisForAll config inputs $ simpleMerge $ f <$> extractUnionExcept v
-- |
-- CEGIS for symbolic programs with error handling, with a forall variable.
--
-- See 'cegisForAll' and 'cegisExceptVC'.
cegisForAllExceptVC ::
( UnionWithExcept t u e v,
UnionPrjOp u,
Monad u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
(Either e v -> u (Either VerificationConditions ())) ->
t ->
IO ([Model], CEGISResult SolvingFailure)
cegisForAllExceptVC config inputs f v = do
cegisForAll config inputs $
simpleMerge $
( \case
Left AssumptionViolation -> cegisPrePost (con False) (con True)
Left AssertionViolation -> cegisPostCond (con False)
_ -> cegisPostCond (con True)
)
<$> (extractUnionExcept v >>= f)
-- |
-- CEGIS for symbolic programs with error handling, with a forall variable.
--
-- See 'cegisForAll' and 'cegisExceptStdVC'.
cegisForAllExceptStdVC ::
( UnionWithExcept t u VerificationConditions (),
UnionPrjOp u,
Monad u,
EvaluateSym inputs,
ExtractSymbolics inputs,
ConfigurableSolver config handle,
SEq inputs
) =>
config ->
inputs ->
t ->
IO ([Model], CEGISResult SolvingFailure)
cegisForAllExceptStdVC config inputs = cegisForAllExceptVC config inputs return