packages feed

liquid-fixpoint-0.9.6.3.4: src/Language/Fixpoint/Smt/Interface.hs

{-# LANGUAGE BangPatterns              #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE RecordWildCards           #-}
{-# LANGUAGE UndecidableInstances      #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE PatternGuards             #-}
{-# LANGUAGE DoAndIfThenElse           #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Use isNothing" #-}

-- | This module contains an SMTLIB2 interface for
--   1. checking the validity, and,
--   2. computing satisfying assignments
--   for formulas.
--   By implementing a binary interface over the SMTLIB2 format defined at
--   http://www.smt-lib.org/
--   http://www.grammatech.com/resource/smt/SMTLIBTutorial.pdf

module Language.Fixpoint.Smt.Interface (

    -- * Commands
      Command  (..)

    -- * Responses
    , Response (..)

    -- * Typeclass for SMTLIB2 conversion
    , SMTLIB2 (..)

    -- * Creating and killing SMTLIB2 Process
    , Context (..)
    , makeContext
    , makeContextNoLog
    , makeContextWithSEnv
    , cleanupContext

    -- * Execute Queries
    , command
    , smtSetMbqi

    -- * Query API
    , smtDecl
    , smtDecls
    , smtDefineFunc
    , smtAssert
    , smtAssertDecl
    , smtFuncDecl
    , smtAssertAxiom
    , smtCheckUnsat
    , smtBracket, smtBracketAt
    , smtDistinct
    , smtPush, smtPop
    , smtComment

    -- * Check Validity
    , checkValid
    , checkValid'
    , checkValidWithContext
    , checkValids

    , funcSortVars

    ) where

import           Language.Fixpoint.Types.Config ( SMTSolver (..), solverFlags
                                                , Config (solver, smtTimeout, noStringTheory, save, allowHO))
import qualified Language.Fixpoint.Misc          as Misc
import           Language.Fixpoint.Types.Errors
import           Language.Fixpoint.Utils.Files
import           Language.Fixpoint.Types         hiding (allowHO)
import qualified Language.Fixpoint.Types         as F
import           Language.Fixpoint.Smt.Types
import qualified Language.Fixpoint.Smt.Theories as Thy
import           Language.Fixpoint.Smt.Serialize ()
import           Control.Applicative      ((<|>))
import           Control.Monad
import           Control.Monad.State
import           Control.Exception
import           Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.Char8 as Char8
import           Data.Char
import qualified Data.HashMap.Strict      as M
import           Data.List                (uncons)
import           Data.Maybe              (fromMaybe)
import qualified Data.Text                as T
import qualified Data.Text.Encoding       as TE
import qualified Data.Text.IO
-- import           Data.Text.Format
import qualified Data.Text.Lazy.IO        as LTIO
import           System.Directory
import           System.Console.CmdArgs.Verbosity
import           System.FilePath
import           System.IO
import qualified Data.Attoparsec.Text     as A
-- import qualified Data.HashMap.Strict      as M
import           Data.Attoparsec.Internal.Types (Parser)
import           Text.PrettyPrint.HughesPJ (text)
import           Language.Fixpoint.SortCheck
import           Language.Fixpoint.Utils.Builder as Builder
-- import qualified Language.Fixpoint.Types as F
-- import           Language.Fixpoint.Types.PrettyPrint (tracepp)
import qualified SMTLIB.Backends
import qualified SMTLIB.Backends.Process as Process
import qualified Language.Fixpoint.Conditional.Z3 as Conditional.Z3
import Control.Concurrent.Async (async)
import GHC.Stack (HasCallStack)

{-
runFile f
  = readFile f >>= runString

runString str
  = runCommands $ rr str

runCommands cmds
  = do me   <- makeContext Z3
       mapM_ (T.putStrLn . smt2) cmds
       zs   <- mapM (command me) cmds
       return zs
-}

checkValidWithContext
  :: HasCallStack => [(Symbol, Sort)] -> Expr -> Expr -> SmtM Bool
checkValidWithContext xts p q =
  smtBracket "checkValidWithContext" $
    checkValid' xts p q

-- | type ClosedPred E = {v:Pred | subset (vars v) (keys E) }
-- checkValid :: e:Env -> ClosedPred e -> ClosedPred e -> IO Bool
checkValid
  :: HasCallStack
  => Config -> FilePath -> [(Symbol, Sort)] -> Expr -> Expr -> IO Bool
checkValid cfg f xts p q = do
  me <- makeContext cfg f
  evalStateT (checkValid' xts p q) me

checkValid' :: HasCallStack => [(Symbol, Sort)] -> Expr -> Expr -> SmtM Bool
checkValid' xts p q = do
  smtDecls xts
  smtAssertDecl $ pAnd [p, PNot q]
  smtCheckUnsat

-- | If you already HAVE a context, where all the variables have declared types
--   (e.g. if you want to make MANY repeated Queries)

-- checkValid :: e:Env -> [ClosedPred e] -> IO [Bool]
checkValids :: Config -> FilePath -> [(Symbol, Sort)] -> [Expr] -> IO [Bool]
checkValids cfg f xts ps = do
  me <- makeContext cfg f
  evalStateT (checkValids' xts ps) me

checkValids' :: [(Symbol, Sort)] -> [Expr] -> SmtM [Bool]
checkValids' xts ps = do
  smtDecls xts
  forM ps $ \p ->
     smtBracket "checkValids" $
       smtAssert (PNot p) >> smtCheckUnsat

-- debugFile :: FilePath
-- debugFile = "DEBUG.smt2"

--------------------------------------------------------------------------------
-- | SMT IO --------------------------------------------------------------------
--------------------------------------------------------------------------------

commandRaw :: Maybe Handle -> SMTLIB.Backends.Solver -> Bool -> Builder -> IO Response
commandRaw ctxLog ctxSolver ctxVerbose cmdBS = do
  resp <- SMTLIB.Backends.command ctxSolver cmdBS
  let respTxt =
        TE.decodeUtf8With (const $ const $ Just ' ') $
        LBS.toStrict resp
  case A.parseOnly responseP respTxt of
    Left e  -> Misc.errorstar $ "SMTREAD:" ++ e
    Right r -> do
      let textResponse = "; SMT Says: " <> T.pack (show r)
      forM_ ctxLog $ \h ->
        Data.Text.IO.hPutStrLn h textResponse
      when ctxVerbose $
        Data.Text.IO.putStrLn textResponse
      return r

--------------------------------------------------------------------------------
{-# SCC command #-}
command  :: HasCallStack => Command -> SmtM Response
--------------------------------------------------------------------------------
command !cmd       = do
  -- whenLoud $ do LTIO.appendFile debugFile (s <> "\n")
  --               LTIO.putStrLn ("CMD-RAW:" <> s <> ":CMD-RAW:DONE")
  ctxLog <- gets ctxLog
  ctxSolver <- gets ctxSolver
  ctxVerbose <- gets ctxVerbose
  cmdBS <- liftSym $ runSmt2 cmd
  forM_ ctxLog $ \h -> lift $ do
    BS.hPutBuilder h cmdBS
    LBS.hPutStr h "\n"
  lift $ case cmd of
    CheckSat   -> commandRaw ctxLog ctxSolver ctxVerbose cmdBS
    GetValue _ -> commandRaw ctxLog ctxSolver ctxVerbose cmdBS
    _          -> SMTLIB.Backends.command_ ctxSolver cmdBS >> return Ok

-- | A variant of `command` that accepts a pre-built command
commandB :: Builder -> SmtM Response
--------------------------------------------------------------------------------
commandB cmdBS       = do
  ctxLog <- gets ctxLog
  ctxSolver <- gets ctxSolver
  forM_ ctxLog $ \h -> lift $ do
    BS.hPutBuilder h cmdBS
    LBS.hPutStr h "\n"
  lift $ SMTLIB.Backends.command_ ctxSolver cmdBS >> return Ok

smtSetMbqi :: SmtM ()
smtSetMbqi = interact' SetMbqi

type SmtParser a = Parser T.Text a

responseP :: SmtParser Response
responseP = {- SCC "responseP" -} A.char '(' *> sexpP
         <|> A.string "sat"     *> return Sat
         <|> A.string "unsat"   *> return Unsat
         <|> A.string "unknown" *> return Unknown

sexpP :: SmtParser Response
sexpP = {- SCC "sexpP" -} A.string "error" *> (Error <$> errorP)
     <|> Values <$> valuesP

errorP :: SmtParser T.Text
errorP = A.skipSpace *> A.char '"' *> A.takeWhile1 (/='"') <* A.string "\")"

valuesP :: SmtParser [(Symbol, T.Text)]
valuesP = A.many1' pairP <* A.char ')'

pairP :: SmtParser (Symbol, T.Text)
pairP = {- SCC "pairP" -}
  do A.skipSpace
     _ <- A.char '('
     !x <- symbolP
     A.skipSpace
     !v <- valueP
     _ <- A.char ')'
     return (x,v)

symbolP :: SmtParser Symbol
symbolP = {- SCC "symbolP" -} symbol <$> A.takeWhile1 (not . isSpace)

valueP :: SmtParser T.Text
valueP = {- SCC "valueP" -} negativeP
      <|> A.takeWhile1 (\c -> not (c == ')' || isSpace c))

negativeP :: SmtParser T.Text
negativeP
  = do v <- A.char '(' *> A.takeWhile1 (/=')') <* A.char ')'
       return $ "(" <> v <> ")"

--------------------------------------------------------------------------
-- | SMT Context ---------------------------------------------------------
--------------------------------------------------------------------------

--------------------------------------------------------------------------
makeContext :: Config -> FilePath -> IO Context
--------------------------------------------------------------------------
makeContext cfg f
  = do mb_hLog <- if not (save cfg) then pure Nothing else do
           createDirectoryIfMissing True $ takeDirectory smtFile
           hLog <- openFile smtFile WriteMode
           hSetBuffering hLog $ BlockBuffering $ Just $ 1024 * 1024 * 64
           return $ Just hLog
       me   <- makeContext' cfg mb_hLog
       pre  <- smtPreamble cfg (solver cfg) me
       forM_ pre $ \line -> do
           SMTLIB.Backends.command_ (ctxSolver me) line
           forM_ mb_hLog $ \hLog -> do
               BS.hPutBuilder hLog line
               LBS.hPutStr hLog "\n"
       return me
    where
       smtFile = extFileName Smt2 f

makeContextWithSEnv :: Config -> FilePath -> SymEnv -> DefinedFuns -> IO Context
makeContextWithSEnv cfg f env defns = do
  ctx      <- makeContext cfg f
  let ctx' = ctx {ctxSymEnv = env, ctxDefines = defns}
  execStateT declare ctx'

makeContextNoLog :: Config -> IO Context
makeContextNoLog cfg = do
  me  <- makeContext' cfg Nothing
  pre <- smtPreamble cfg (solver cfg) me
  mapM_ (SMTLIB.Backends.command_ (ctxSolver me)) pre
  return me

makeProcess
  :: Maybe Handle
  -> Process.Config
  -> IO (SMTLIB.Backends.Backend, IO ())
makeProcess ctxLog cfg
  = do handl@Process.Handle {hMaybeErr = Just hErr, ..} <- Process.new cfg
       case ctxLog of
         Nothing -> return ()
         Just hLog -> void $ async $ forever
           (do errTxt <- LTIO.hGetLine hErr
               LTIO.hPutStrLn hLog $ "OOPS, SMT solver error:" <> errTxt
           ) `catch` \ SomeException {} -> return ()
       let backend = Process.toBackend handl
       hSetBuffering hOut $ BlockBuffering $ Just $ 1024 * 1024 * 64
       hSetBuffering hIn $ BlockBuffering $ Just $ 1024 * 1024 * 64
       return (backend, Process.close handl)

makeContext' :: Config -> Maybe Handle -> IO Context
makeContext' cfg ctxLog
  = do let slv = solver cfg
       (backend, closeIO) <- case slv of
         Z3      ->
           {- "z3 -smt2 -in"                   -}
           {- "z3 -smtc SOFT_TIMEOUT=1000 -in" -}
           {- "z3 -smtc -in MBQI=false"        -}
           makeProcess ctxLog $ Process.defaultConfig
                             { Process.exe = "z3"
                             , Process.args = ["-smt2", "-in"] }
         Z3mem   -> Conditional.Z3.makeZ3
         Mathsat -> makeProcess ctxLog $ Process.defaultConfig
                             { Process.exe = "mathsat"
                             , Process.args = ["-input=smt2"] }
         Cvc4    -> makeProcess ctxLog $
                      Process.defaultConfig
                             { Process.exe = "cvc4"
                             , Process.args = ["-L", "smtlib2"] }
         Cvc5    -> makeProcess ctxLog $
                      Process.defaultConfig
                             { Process.exe = "cvc5"
                             , Process.args = ["-L", "smtlib2", "--arrays-exp"] }
       solver <- SMTLIB.Backends.initSolver SMTLIB.Backends.Queuing backend
       loud <- isLoud
       return Ctx { ctxSolver    = solver
                  , ctxElabF     = solverFlags cfg
                  , ctxClose     = closeIO
                  , ctxLog       = ctxLog
                  , ctxVerbose   = loud
                  , ctxSymEnv    = mempty
                  , ctxIxs       = []
                  , ctxDefines   = mempty
                  -- This is a heurstic to avoid generating large sequences of unused `lam_arg` symbols
                  -- when there's no higher-order reasoning. It might require some tuning on larger codebases
                  -- if `unknown function/constant lam_arg$XXX` errors are encountered.
                  , ctxLams      = allowHO cfg
                  , config       = cfg
                  }

-- | Close file handles and release the solver backend's resources.
cleanupContext :: Context -> IO ()
cleanupContext Ctx {..} = do
  maybe (return ()) (hCloseMe "ctxLog") ctxLog
  ctxClose

hCloseMe :: String -> Handle -> IO ()
hCloseMe msg h = hClose h `catch` (\(exn :: IOException) -> putStrLn $ "OOPS, hClose breaks: " ++ msg ++ show exn)

smtPreamble :: Config -> SMTSolver -> Context -> IO [Builder]
smtPreamble cfg s me
  | s == Z3 || s == Z3mem
    = do v <- getZ3Version me
         checkValidStringFlag Z3 v cfg
         return $ makeMbqi ++ makeTimeout cfg ++ Thy.preamble cfg Z3
  | otherwise
    = checkValidStringFlag s [] cfg >> return (Thy.preamble cfg s)
  where
    makeMbqi = ["\n(set-option :smt.mbqi false)"]

getZ3Version :: Context -> IO [Int]
getZ3Version me
  = do -- resp is like (:version "4.8.15")
       resp <- SMTLIB.Backends.command (ctxSolver me) "(get-info :version)"
       case Char8.split '"' resp of
         _:rText:_ -> do
           -- strip off potential " - build hashcode ..." suffix
           let vText = Char8.takeWhile (not . isSpace) rText
           let parsedComponents = [ reads (Char8.unpack cText) | cText <- Char8.split '.' vText ]
           sequence
             [ case pComponent of
                 [(c, "")] -> return c
                 xs -> error $ "Can't parse z3 version: " ++ show xs
             | pComponent <- parsedComponents
             ]
         xs -> error $ "Can't parse z3 (get-info :version): " ++ show xs

checkValidStringFlag :: SMTSolver -> [Int] -> Config -> IO ()
checkValidStringFlag smt v cfg
  = when (noString smt v cfg) $
      die $ err dummySpan (text "stringTheory is only supported by z3 version >=4.2.2")

noString :: SMTSolver -> [Int] -> Config -> Bool
noString smt v cfg
  =  not (noStringTheory cfg)
  && not (smt == Cvc5 || (smt == Z3 && (v >= [4, 4, 2])))
-----------------------------------------------------------------------------
-- | SMT Commands -----------------------------------------------------------
-----------------------------------------------------------------------------

smtPush, smtPop :: SmtM ()
smtPush = interact' Push
smtPop  = interact' Pop

smtComment :: T.Text -> SmtM ()
smtComment t = interact' (Comment t)

smtDecls :: [(Symbol, Sort)] -> SmtM ()
smtDecls = mapM_ $ uncurry smtDecl

smtDecl :: Symbol -> Sort -> SmtM ()
smtDecl x t = do
  me <- get
  let env = seData (ctxSymEnv me)
  let ins' = sortSmtSort False env <$> ins
  let out' = sortSmtSort False env     out
  interact' (notracepp _msg $ Declare (symbolSafeText x) ins' out')
  where
    (ins, out) = deconSort t
    _msg       = "smtDecl: " ++ showpp (x, t, ins, out)

smtFuncDecl :: T.Text -> ([SmtSort],  SmtSort) -> SmtM ()
smtFuncDecl x (ts, t) = interact' (Declare x ts t)

smtDataDecl :: [DataDecl] -> SmtM ()
smtDataDecl ds = interact' (DeclData ds)

deconSort :: Sort -> ([Sort], Sort)
deconSort t = case functionSort t of
                Just (_, ins, out) -> (ins, out)
                Nothing            -> ([], t)

smtAssert :: Expr -> SmtM ()
smtAssert p = interact' (Assert Nothing p)

-- the following three functions will emit additional `apply`,
-- `coerce`, and `lambda` symbols for fresh function sorts as needed
smtAssertDecl :: HasCallStack => Expr -> SmtM ()
smtAssertDecl p = interactDecl' (Assert Nothing p)

smtDefineEqn :: Equation -> SmtM ()
smtDefineEqn Equ {..} = smtDefineFunc eqName eqArgs eqSort eqBody

smtDefineFunc :: Symbol -> [(Symbol, F.Sort)] -> F.Sort -> Expr -> SmtM ()
smtDefineFunc name symList rsort e =
  do env <- gets (seData . ctxSymEnv)
     interactDecl' $
        DefineFunc
          name
          (map (sortSmtSort False env <$>) symList)
          (sortSmtSort False env rsort)
          e

-----------------------------------------------------------------

smtAssertAxiom :: Triggered Expr -> SmtM ()
smtAssertAxiom p  = interact' (AssertAx p)

smtDistinct :: [Expr] -> SmtM ()
smtDistinct az = interact' (Distinct az)

smtCheckUnsat :: HasCallStack => SmtM Bool
smtCheckUnsat = respSat <$> command CheckSat

smtBracketAt :: SrcSpan -> String -> SmtM a -> SmtM a
smtBracketAt sp _msg a =
  smtBracket _msg a `catchSMT` dieAt sp

-- | `smtBracket` adds a new level to the apply stack and saves the last fresh index
--   on the index stack before the action, and reverts these changes after the action.
smtBracket :: String -> SmtM a -> SmtM a
smtBracket msg a = do
  smtComment (T.pack $ "smtBracket - start: " ++ msg)
  smtPush
  modify $ \ctx ->
    let env = ctxSymEnv ctx in
    ctx { ctxSymEnv = env { seAppls = pushAppls (seAppls env) }
        , ctxIxs = seIx env : ctxIxs ctx}
  r <- a
  smtPop
  smtComment (T.pack $ "smtBracket - end: " ++ msg)
  modify $ \ctx ->
    let env = ctxSymEnv ctx
        (i , is) = fromMaybe (0, []) (uncons $ ctxIxs ctx)
      in
    ctx { ctxSymEnv = env {seAppls = popAppls (seAppls env) , seIx = i}
        , ctxIxs = is}
  return r

respSat :: HasCallStack => Response -> Bool
respSat Unsat   = True
respSat Sat     = False
respSat Unknown = False
respSat r       = die $ err dummySpan $ text ("crash: SMTLIB2 respSat = " ++ show r)

interact' :: Command -> SmtM ()
interact' cmd  = void $ command cmd

-- | a variant of `interact'` which also emits fresh
--   `apply`, `coerce`, and `lambda` symbols
interactDecl' :: HasCallStack => Command -> SmtM ()
interactDecl' cmd  = do
  cmdBS <- liftSym $ runSmt2 cmd
  ctx <- get
  let env = ctxSymEnv ctx
  let ats = funcSortVars (ctxLams ctx) env
  forM_ ats $ uncurry smtFuncDecl
  put (ctx {ctxSymEnv = env {seAppls = mergeTopAppls (seApplsCur env) (seAppls env), seApplsCur = M.empty} })
  void $ commandB cmdBS

makeTimeout :: Config -> [Builder]
makeTimeout cfg
  | Just i <- smtTimeout cfg = [ "\n(set-option :timeout " <> fromString (show i) <> ")\n"]
  | otherwise                = [""]


--------------------------------------------------------------------------------
declare :: SmtM ()
--------------------------------------------------------------------------------
declare = do
  me <- get
  let env        = ctxSymEnv me
  let xts        = symbolSorts (F.seSort env)
  let tx         = elaborate (ElabParam (ctxElabF me) "declare" env)
  let lts        = F.toListSEnv . F.seLits $ env
  let dss        = dataDeclarations          env
  let thyXTs     =             [ (x, t) | (x, t) <- xts, symKind env x == Just F.Uninterp ]
  let qryXTs     = fmap tx <$> [ (x, t) | (x, t) <- xts, symKind env x == Nothing ]
  -- let isKind n   = (n ==)  . symKind env . fst
  let MkDefinedFuns defs = ctxDefines me
  let ess        = distinctLiterals  lts
  let axs        = Thy.axiomLiterals (config me) lts
  forM_ dss              smtDataDecl
  forM_ thyXTs $ uncurry smtDecl
  forM_ qryXTs $ uncurry smtDecl
  forM_ defs             smtDefineEqn
  forM_ ess              smtDistinct
  forM_ axs              smtAssert

symbolSorts :: F.SEnv F.Sort -> [(F.Symbol, F.Sort)]
symbolSorts env = [(x, tx t) | (x, t) <- F.toListSEnv env ]
 where
  tx t@(FObj a) = fromMaybe t (F.lookupSEnv a env)
  tx t          = t

dataDeclarations :: SymEnv -> [[DataDecl]]
dataDeclarations = orderDeclarations . map snd . F.toListSEnv . F.seData

-- | See 'F.seApplsCur' for explanation.
funcSortVars :: Bool -> F.SymEnv -> [(T.Text, ([F.SmtSort], F.SmtSort))]
funcSortVars lams env =
    concatMap symbolsForTag $ M.toList $ F.seApplsCur env
  where
    symbolsForTag (t, i) =
      let applySym  = symbolAtSortIndex applyName i
          coerceSym = symbolAtSortIndex coerceName i
          lamSym    = symbolAtSortIndex lambdaName i
          argSyms   = if lams && snd t == F.SInt
                        then [ (symbolAtSortIndex (lamArgSymbol j) i, argSort t)
                             | j <- [1..Thy.maxLamArg] ]
                        else []
      in  (applySym, appSort t)
        : (coerceSym, ([fst t], snd t))
        : (lamSym, lamSort t)
        : argSyms

    appSort (s,t) = ([F.SInt, s], t)
    lamSort (s,t) = ([s, t], F.SInt)
    argSort (s,_) = ([]    , s)

symKind :: F.SymEnv -> F.Symbol -> Maybe Sem
symKind env x = F.tsInterp <$> F.symEnvTheory x env

-- | `distinctLiterals` is used solely to determine the set of literals
--   (of each sort) that are *disequal* to each other, e.g. EQ, LT, GT,
--   or string literals "cat", "dog", "mouse". These should only include
--   non-function sorted values.
distinctLiterals :: [(F.Symbol, F.Sort)] -> [[F.Expr]]
distinctLiterals xts = [ es | (_, es) <- tess ]
   where
    tess             = Misc.groupList [(t, F.expr x) | (x, t) <- xts, notFun t]
    notFun           = not . F.isFunctionSortedReft . (`F.RR` F.trueReft)
    -- _notStr          = not . (F.strSort ==) . F.sr_sort . (`F.RR` F.trueReft)