packages feed

LPPaver-0.0.5.0: app/LPPaver.hs

module Main where

import MixedTypesNumPrelude
import AERN2.MP.Ball
import LPPaver.Algorithm.DNF
import PropaFP.Expression
import PropaFP.Eliminator
import PropaFP.VarMap
import PropaFP.Parsers.Smt (parseSMT2, parseVCToF, ParsingMode (Why3, CNF))
import PropaFP.Parsers.DRealSmt
import Options.Applicative
import System.Directory
import System.FilePath
import Data.Ratio
import LPPaver.Algorithm.Util
import LPPaver.Algorithm.Type
import Control.Monad

data ProverOptions = ProverOptions
  {
    provingProcessDone :: Bool,
    ceMode :: Bool,
    depthCutoff :: Integer,
    bestFirstSearchCutoff :: Integer,
    precision :: Integer,
    -- relativeImprovementCutoff :: Rational, make this a flag, as a double is probably easier
    fileName :: String,
    outputPavings :: Bool
  }

proverOptions :: Parser ProverOptions
proverOptions = ProverOptions
  <$> switch
    (
      long "propafp-done"
      <> short 'a'
      <> help "Add this option if the target file was generated by PropaFP"
    )
  <*> switch
    (
      long "ce-mode"
      <> short 'c'
      <> help "Tell LPPaver to focus on finding a counterexample"
    )
  <*> option auto
    (
      long "depth-cutoff"
      <> short 'd'
      <> help "How hard LPPaver 'works' until giving up. This option is ignored when ce-mode is on"
      <> showDefault
      <> value 100
      <> metavar "INT"
    )
  <*> option auto
    (
      long "best-first-search-cutoff"
      <> short 'b'
      <> help "How hard LPPaver 'works' until giving up when searching for a counterexample. This option is ignored when ce-mode is off"
      <> showDefault
      <> value 1000
      <> metavar "INT"
    )
  <*> option auto
    (
      long "precision"
      <> short 'p'
      <> help "Precision of floating-point numbers used within the prover. Higher precision slows down the prover but may be needed for more difficult problems"
      <> showDefault
      <> value 100
      <> metavar "INT"
    )
  <*> strOption
    (
      long "file-path"
      <> short 'f'
      <> help "SMT2 file to be checked"
      <> metavar "filePath"
    )
  <*> switch
    (
      long "output-pavings"
      <> short 'o'
      <> help "When this flag is passed, LPPaver will produce JSON output of paved boxes."
    )

main :: IO ()
main =
  do
    runProver =<< execParser opts
    where
      opts = info (proverOptions <**> helper)
        ( fullDesc
        <> progDesc "todo"
        <> header "LPPaver - prover" )

runProver :: ProverOptions -> IO ()
runProver proverOptions@(ProverOptions provingProcessDone ceMode depthCutoff bestFirstSearchCutoff p filePath outputPavings) =
  do
    if provingProcessDone
      then do
        parsedFile <- parseSMT2 filePath
        case parseDRealSmtToF parsedFile of
          (Just vc, typedVarMap) ->
            let
              -- If there are variable free comparisons here, we could not deal with them earlier in the proving process.
              -- LPPaver cannot perform any better with these so we safely remove them.
              ednf = fDNFToEDNF . simplifyFDNF . fToFDNF . simplifyF . minMaxAbsEliminatorF . simplifyF . removeVariableFreeComparisons $ vc
            in do
              decideEDNFWithVarMap ednf typedVarMap proverOptions
          (_, _) -> error "Error - Issue parsing given SMT file"
      else do
        -- PATH needs to include folder containing FPTaylor binary after make
        -- symlink to the binary in somewhere like ~/.local/bin will NOT work reliably
        mFptaylorPath <- findExecutable "fptaylor"
        case mFptaylorPath of
          Nothing -> putStrLn "Error - fptaylor executable not in path"
          Just fptaylorPath -> do
            mParsedVC <- parseVCToF filePath fptaylorPath
            case mParsedVC of
              Just (vc, typedVarMap) ->
                let
                  -- If there are variable free comparisons here, we could not deal with them earlier in the proving process.
                  -- LPPaver cannot perform any better with these so we safely remove them.
                  ednf = fDNFToEDNF . simplifyFDNF . fToFDNF . simplifyF . minMaxAbsEliminatorF . simplifyF . removeVariableFreeComparisons $ vc
                in do
                  decideEDNFWithVarMap ednf typedVarMap proverOptions
              Nothing -> do
                putStrLn "unknown"
                putStrLn "Issue parsing file"

decideEDNFWithVarMap :: [[ESafe]] -> TypedVarMap -> ProverOptions -> IO ()
decideEDNFWithVarMap ednf typedVarMap (ProverOptions provingProcessDone ceMode depthCutoff bestFirstSearchCutoff p filePath outputPavings) = do
  let result =
        if ceMode
          then checkEDNFBestFirstWithSimplexCE ednf typedVarMap bestFirstSearchCutoff 1.2 (prec p)
          else checkEDNFDepthFirstWithSimplex  ednf typedVarMap depthCutoff           1.2 (prec p)
  let vcFileWithoutExtension = takeFileName . dropExtensions $ filePath
  case result of
    SatDNF model pavings -> do
      putStrLn "sat"
      printSMTModel model
      prettyPrintCounterExample model
      when outputPavings $ do
        let jsonOutput = typedVarMapBoxPavingsToJSON pavings 0
        writeJSONFile vcFileWithoutExtension jsonOutput
    UnsatDNF listOfPavings -> do
      putStrLn "unsat"
      when outputPavings $ do
        let jsonOutput = listOfTypedVarMapPavingsToJSON listOfPavings 0
        writeJSONFile vcFileWithoutExtension jsonOutput
    IndeterminateDNF indeterminateExample pavings -> do
      putStrLn "unknown"
      printSMTModel indeterminateExample
      prettyPrintCounterExample indeterminateExample
      when outputPavings $ do
        let jsonOutput = typedVarMapBoxPavingsToJSON pavings 0
        writeJSONFile vcFileWithoutExtension jsonOutput
  where
    -- Useful functions for converting our pavings into a JSON object

    rationalToJSON :: Rational -> String
    rationalToJSON r = "[" ++ show n ++ "," ++ show d ++ "]"
      where
        n = numerator r
        d = denominator r

    listOfTypedVarMapPavingsToJSON :: [[BoxStep TypedVarMap]] -> Integer -> String
    listOfTypedVarMapPavingsToJSON listOfPavings tabCount =
      replicate tabCount '\t' ++ "[" ++ "\n" ++
      aux listOfPavings ++ "\n" ++
      replicate tabCount '\t' ++ "]"
      where
        aux [] = ""
        aux [ps] = typedVarMapBoxPavingsToJSON ps 1
        aux (ps : pss) = aux [ps] ++ ",\n" ++ aux pss

    typedVarMapBoxPavingsToJSON :: [BoxStep TypedVarMap] -> Integer -> String
    typedVarMapBoxPavingsToJSON pavings tabCount =
      replicate tabCount '\t' ++ "[" ++ "\n" ++
      aux pavings ++ "\n" ++
      replicate tabCount '\t' ++ "]"
      where
        tabCount' = tabCount + 1

        aux [] = ""
        aux [p] = typedVarMapBoxPavingToJSON p tabCount'
        aux (p : ps) = aux [p] ++ replicate tabCount' '\t' ++ ",\n" ++ aux ps

    typedVarMapBoxPavingToJSON :: BoxStep TypedVarMap -> Integer -> String
    typedVarMapBoxPavingToJSON boxStep tabCount =
      replicate tabCount '\t' ++ "{\n" ++
      aux boxStep ++
      replicate tabCount '\t' ++ "}\n"
      where
        tabCount' = tabCount + 1
        tabCount'' = tabCount' + 1
        tabCount''' = tabCount'' + 1
        tabCount'''' = tabCount''' + 1

        showTypedVarMap box boxName = 
          replicate tabCount' '\t' ++ "\"" ++ boxName ++ "\":\n" ++
          replicate tabCount'' '\t' ++ "[\n" ++
          aux box ++
          replicate tabCount'' '\t' ++ "]"
          where
            aux [] = ""
            aux [TypedVar (v, (l, r)) t] =
              replicate tabCount''' '\t' ++ "{\n" ++
              replicate tabCount'''' '\t' ++ "\"variableName\" : " ++ show v ++ ",\n" ++
              replicate tabCount'''' '\t' ++ "\"variableType\" : " ++ "\"" ++ show t ++ "\"" ++ ",\n" ++
              replicate tabCount'''' '\t' ++ "\"leftEndpoint\" : " ++ rationalToJSON l ++ ",\n" ++
              replicate tabCount'''' '\t' ++ "\"rightEndpoint\" : " ++ rationalToJSON r ++ "\n" ++
              replicate tabCount''' '\t' ++ "}\n"
            aux (v : vs) = aux [v] ++ replicate tabCount''' '\t' ++ "," ++ "\n" ++ aux vs

        aux (Initial box) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"Initial\",\n" ++
          showTypedVarMap box "box" ++ "\n"
        aux (EvalTrue box) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"EvalTrue\",\n" ++
          showTypedVarMap box "box" ++ "\n"
        aux (EvalFalse box) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"EvalFalse\",\n" ++
          showTypedVarMap box "box" ++ "\n"
        aux (GaveUp box) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"GaveUp\",\n" ++
          showTypedVarMap box "box" ++ "\n"
        aux (ContractEmpty box) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"ContractEmpty\",\n" ++
          showTypedVarMap box "box" ++ "\n"
        aux (Contract box boxC) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"Contract\",\n" ++
          showTypedVarMap box "box" ++ ",\n" ++
          showTypedVarMap boxC "boxC" ++ "\n"
        aux (FoundModel box boxM) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"FoundModel\",\n" ++
          showTypedVarMap box "box" ++ ",\n" ++
          showTypedVarMap boxM "boxM" ++ "\n"
        aux (Split box boxL boxR) =
          replicate tabCount' '\t' ++ "\"boxStep\": \"Split\",\n" ++
          showTypedVarMap box "box" ++ ",\n" ++
          showTypedVarMap boxL "boxL" ++ ",\n" ++
          showTypedVarMap boxR "boxR" ++ "\n"

    writeJSONFile :: String -> String -> IO ()
    writeJSONFile fileNameWithoutExtension jsonString = do
      putStr "Saving pavings information to "
      safeFile >>= putStrLn
      safeFile >>= (`writeFile` jsonString)
      putStrLn "Pavings information saved"
      where
        safeFile = do
          let jsonFileName = fileNameWithoutExtension ++ ".json"
          doesJSONFileNameExist <- doesFileExist jsonFileName
          if doesJSONFileNameExist
            then findSafeFile fileNameWithoutExtension 0
            else return jsonFileName

        -- |Chooses an empty/non-existant file to write to
        findSafeFile f appendCounter = do
          let currentF = f ++ show appendCounter ++ ".json"
          doesCurrentFExist <- doesFileExist currentF
          if doesCurrentFExist
            then findSafeFile f (appendCounter + 1)
            else return currentF

prettyPrintCounterExample :: TypedVarMap -> IO ()
prettyPrintCounterExample [] = return ()
prettyPrintCounterExample ((TypedVar (v, (l, r)) t) : vs) =
  if l == r
    then do
      putStrLn (v ++ " = " ++ show (double l))
      prettyPrintCounterExample vs
    else do
      putStrLn (v ++ " = [" ++ show (double l) ++ ", " ++ show (double r) ++ "]")
      prettyPrintCounterExample vs

printSMTModel :: TypedVarMap -> IO ()
printSMTModel typedVarMap =
  do
    putStrLn "(model"
    printModels typedVarMap
    putStrLn ")"
  where
    printModels [] = return ()
    printModels ((TypedVar (v, (l, r)) t) : vs) = do
      putStrLn $ "(define-fun " ++ v ++ " () " ++ show t ++ " " ++ showNum (l) ++ ")"
      putStrLn $ "(define-fun " ++ v ++ "_vc_constant" ++ " () " ++ show t ++ " " ++ showNum (l) ++ ")" --FIXME: Only do this with a Why3 or similar flag?
      printModels vs

    showNum :: Rational -> String
    showNum num =
      if num < 0
        then "(/ " ++ "(" ++ show (numerator num) ++ ") " ++ show (denominator num) ++ ")"
        else "(/ " ++ show (numerator num) ++ " " ++ show (denominator num) ++ ")"