{-|
Module      : What4.Symbol
Description : Datatype for representing names that can be communicated to solvers
Copyright   : (c) Galois Inc, 2015-2020
License     : BSD3
Maintainer  : jhendrix@galois.com

This defines a datatype for representing identifiers that can be
used with Crucible.  These must start with an ASCII letter and can consist
of any characters in the set @['a'-'z' 'A'-'Z' '0'-'9' '_']@ as long as the
result is not an SMTLIB or Yices keyword.
-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module What4.Symbol
  ( SolverSymbol
  , solverSymbolAsText
  , SolverSymbolError
  , emptySymbol
  , userSymbol
  , systemSymbol
  , safeSymbol
  , ppSolverSymbolError
  ) where

import           Data.Char
import           Data.Hashable
import           Data.Set (Set)
import qualified Data.Set as Set
import           Data.String
import           Data.Text (Text)
import qualified Data.Text as Text

import qualified Text.Encoding.Z as Z

isAsciiLetter :: Char -> Bool
isAsciiLetter c
  =  'A' <= c && c <= 'Z'
  || 'a' <= c && c <= 'z'

isSymbolChar :: Char -> Bool
isSymbolChar c
  = isAsciiLetter c
  || isDigit c
  || c == '_'
  || c == '\''
  || c == '!'

-- | This describes why a given text value was not a valid solver symbol.
data SolverSymbolError
   = SymbolEmpty
   | SymbolNoStartWithChar
   | SymbolIllegalChar
   | SymbolSMTLIBReserved
   | SymbolYicesReserved

instance Show SolverSymbolError where
  show e = "Identifier " ++ ppSolverSymbolError e


ppSolverSymbolError :: SolverSymbolError -> String
ppSolverSymbolError e =
  case e of
    SymbolEmpty           -> "cannot be empty."
    SymbolNoStartWithChar -> "must start with a letter."
    SymbolIllegalChar     -> "contains an illegal character."
    SymbolSMTLIBReserved  -> "is an SMTLIB reserved word."
    SymbolYicesReserved   -> "is a Yices reserved word."


-- | This represents a name known to the solver.
--
-- We have three types of symbols:
--
-- * The empty symbol
--
-- * A user symbol
--
-- * A system symbol
--
-- A user symbol should consist of a letter followed by any combination
-- of letters, digits, and underscore characters.  It also cannot be a reserved
-- word in Yices or SMTLIB.
--
-- A system symbol should start with a letter followed by any number of
-- letter, digit, underscore or an exclamation mark characters.  It must
-- contain at least one exclamation mark to distinguish itself from user
-- symbols.
newtype SolverSymbol = SolverSymbol { solverSymbolAsText :: Text }
  deriving (Eq, Ord, Hashable)

-- | Return the empty symbol.
emptySymbol :: SolverSymbol
emptySymbol = SolverSymbol Text.empty

-- | This returns either a user symbol or the empty symbol if the string is empty.
userSymbol :: String -> Either SolverSymbolError SolverSymbol
userSymbol s
  | elem '!' s = Left SymbolIllegalChar
  | otherwise = parseAnySymbol s

systemSymbol :: String -> SolverSymbol
systemSymbol s
    -- System symbols must contain an exclamation mark to distinguish them from
    -- user symbols (which are not allowed to have exclamation marks).
  | '!' `notElem` s =
    error $
      "The system symbol " ++ show s ++ " must contain at least one exclamation mark '!'"
  | otherwise =
    case parseAnySymbol s of
      Left e -> error ("Error parsing system symbol " ++ show s ++ ": " ++ ppSolverSymbolError e)
      Right r -> r


-- | Attempts to create a user symbol from the given string.  If this fails
--   for some reason, the string is Z-encoded into a system symbol instead
--   with the prefix \"zenc!\".
safeSymbol :: String -> SolverSymbol
safeSymbol str =
  case userSymbol str of
    Right s -> s
    Left _err -> systemSymbol ("zenc!" ++ Z.zEncodeString str)

instance Show SolverSymbol where
  show s = Text.unpack (solverSymbolAsText s)

-- | This attempts to parse a string as a valid solver symbol.
parseAnySymbol :: String -> Either SolverSymbolError SolverSymbol
parseAnySymbol [] = Right emptySymbol
parseAnySymbol (h:r)
  | isAsciiLetter h == False          = Left SymbolNoStartWithChar
  | all isSymbolChar r == False       = Left SymbolIllegalChar
  | t `Set.member` smtlibKeywordSet   = Left SymbolSMTLIBReserved
  | t `Set.member` yicesKeywordSet    = Left SymbolYicesReserved
  | otherwise = Right (SolverSymbol t)
  where t = if elem '\'' r
            then fromString ("|" ++ (h:r) ++ "|")
            else fromString (h:r)

smtlibKeywordSet :: Set Text
smtlibKeywordSet = Set.fromList (fromString <$> smtlibKeywords)

yicesKeywordSet :: Set Text
yicesKeywordSet = Set.fromList (fromString <$> yicesKeywords)

-- | This is the list of keywords in SMTLIB 2.5
smtlibKeywords :: [String]
smtlibKeywords =
  [ "BINARY"
  , "DECIMAL"
  , "HEXADECIMAL"
  , "NUMERAL"
  , "STRING"
  , "as"
  , "let"
  , "exists"
  , "forall"
  , "par"
  , "assert"
  , "check-sat"
  , "check-sat-assuming"
  , "declare-const"
  , "declare-fun"
  , "declare-sort"
  , "define-fun"
  , "define-fun-rec"
  , "define-funs-rec"
  , "define-sort"
  , "echo"
  , "exit"
  , "get-assertions"
  , "get-assignment"
  , "get-info"
  , "get-model"
  , "get-option"
  , "get-proof"
  , "get-unsat-assumptions"
  , "get-unsat-core"
  , "get-value"
  , "pop"
  , "push"
  , "reset"
  , "reset-assertions"
  , "set-info"
  , "set-logic"
  , "set-option"
  ]

yicesKeywords :: [String]
yicesKeywords =
  [ "abs"
  , "and"
  , "assert"
  , "bit"
  , "bitvector"
  , "bool"
  , "bool-to-bv"
  , "bv-add"
  , "bv-and"
  , "bv-ashift-right"
  , "bv-ashr"
  , "bv-comp"
  , "bv-concat"
  , "bv-div"
  , "bv-extract"
  , "bv-ge"
  , "bv-gt"
  , "bv-le"
  , "bv-lshr"
  , "bv-lt"
  , "bv-mul"
  , "bv-nand"
  , "bv-neg"
  , "bv-nor"
  , "bv-not"
  , "bv-or"
  , "bv-pow"
  , "bv-redand"
  , "bv-redor"
  , "bv-rem"
  , "bv-repeat"
  , "bv-rotate-left"
  , "bv-rotate-right"
  , "bv-sdiv"
  , "bv-sge"
  , "bv-sgt"
  , "bv-shift-left0"
  , "bv-shift-left1"
  , "bv-shift-right0"
  , "bv-shift-right1"
  , "bv-shl"
  , "bv-sign-extend"
  , "bv-sle"
  , "bv-slt"
  , "bv-smod"
  , "bv-srem"
  , "bv-sub"
  , "bv-xnor"
  , "bv-xor"
  , "bv-zero-extend"
  , "ceil"
  , "check"
  , "define"
  , "define-type"
  , "distinct"
  , "div"
  , "divides"
  , "dump-context"
  , "echo"
  , "ef-solve"
  , "eval"
  , "exists"
  , "exit"
  , "export-to-dimacs"
  , "false"
  , "floor"
  , "forall"
  , "help"
  , "if"
  , "include"
  , "int"
  , "is-int"
  , "ite"
  , "lambda"
  , "let"
  , "mk-bv"
  , "mk-tuple"
  , "mod"
  , "not"
  , "or"
  , "pop"
  , "push"
  , "real"
  , "reset"
  , "reset-stats"
  , "scalar"
  , "select"
  , "set-param"
  , "set-timeout"
  , "show-implicant"
  , "show-model"
  , "show-param"
  , "show-params"
  , "show-stats"
  , "true"
  , "tuple"
  , "tuple-update"
  , "update"
  , "xor"
  ]
