{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module What4.Config
(
ConfigOption
, configOption
, configOptionType
, configOptionName
, configOptionText
, configOptionNameParts
, OptionSetting(..)
, Opt(..)
, OptionStyle(..)
, set_opt_default
, set_opt_onset
, OptionSetResult(..)
, optOK
, optWarn
, optErr
, checkOptSetResult
, Bound(..)
, boolOptSty
, integerOptSty
, realOptSty
, stringOptSty
, realWithRangeOptSty
, realWithMinOptSty
, realWithMaxOptSty
, integerWithRangeOptSty
, integerWithMinOptSty
, integerWithMaxOptSty
, enumOptSty
, listOptSty
, executablePathOptSty
, ConfigDesc
, mkOpt
, opt
, optV
, optU
, optUV
, Config
, initialConfig
, extendConfig
, getOptionSetting
, getOptionSettingFromText
, ConfigValue(..)
, getConfigValues
, configHelp
, verbosity
, verbosityLogger
) where
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail( MonadFail )
#endif
import Control.Applicative (Const(..))
import Control.Exception
import Control.Lens ((&))
import Control.Monad.Identity
import Control.Monad.IO.Class
import Control.Monad.Writer.Strict hiding ((<>))
import Data.Kind
import Data.Maybe
import Data.Typeable
import Data.Foldable (toList)
import Data.IORef
import Data.List.NonEmpty (NonEmpty(..))
import Data.Parameterized.Some
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as Text
import Numeric.Natural
import System.IO ( Handle, hPutStr )
import System.IO.Error ( ioeGetErrorString )
import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))
import What4.BaseTypes
import What4.Concrete
import qualified What4.Utils.Environment as Env
import What4.Utils.StringLiteral
data ConfigOption (tp :: BaseType) where
ConfigOption :: BaseTypeRepr tp -> NonEmpty Text -> ConfigOption tp
instance Show (ConfigOption tp) where
show = configOptionName
configOption :: BaseTypeRepr tp -> String -> ConfigOption tp
configOption tp nm =
case splitPath (Text.pack nm) of
Just ps -> ConfigOption tp ps
Nothing -> error "config options cannot have an empty name"
splitPath :: Text -> Maybe (NonEmpty Text)
splitPath nm =
let nms = Text.splitOn "." nm in
case nms of
(x:xs) | all (not . Text.null) (x:xs) -> Just (x:|xs)
_ -> Nothing
configOptionNameParts :: ConfigOption tp -> [Text]
configOptionNameParts (ConfigOption _ (x:|xs)) = x:xs
configOptionName :: ConfigOption tp -> String
configOptionName = Text.unpack . configOptionText
configOptionText :: ConfigOption tp -> Text
configOptionText (ConfigOption _ (x:|xs)) = Text.intercalate "." $ (x:xs)
configOptionType :: ConfigOption tp -> BaseTypeRepr tp
configOptionType (ConfigOption tp _) = tp
data OptionSetResult =
OptionSetResult
{ optionSetError :: !(Maybe Doc)
, optionSetWarnings :: !(Seq Doc)
}
instance Semigroup OptionSetResult where
x <> y = OptionSetResult
{ optionSetError = optionSetError x <> optionSetError y
, optionSetWarnings = optionSetWarnings x <> optionSetWarnings y
}
instance Monoid OptionSetResult where
mappend = (<>)
mempty = optOK
optOK :: OptionSetResult
optOK = OptionSetResult{ optionSetError = Nothing, optionSetWarnings = mempty }
optErr :: Doc -> OptionSetResult
optErr x = OptionSetResult{ optionSetError = Just x, optionSetWarnings = mempty }
optWarn :: Doc -> OptionSetResult
optWarn x = OptionSetResult{ optionSetError = Nothing, optionSetWarnings = Seq.singleton x }
data OptionSetting (tp :: BaseType) =
OptionSetting
{ optionSettingName :: ConfigOption tp
, getOption :: IO (Maybe (ConcreteVal tp))
, setOption :: ConcreteVal tp -> IO OptionSetResult
}
data OptionStyle (tp :: BaseType) =
OptionStyle
{ opt_type :: BaseTypeRepr tp
, opt_onset :: Maybe (ConcreteVal tp) -> ConcreteVal tp -> IO OptionSetResult
, opt_help :: Doc
, opt_default_value :: Maybe (ConcreteVal tp)
}
defaultOpt :: BaseTypeRepr tp -> OptionStyle tp
defaultOpt tp =
OptionStyle
{ opt_type = tp
, opt_onset = \_ _ -> return mempty
, opt_help = empty
, opt_default_value = Nothing
}
set_opt_onset :: (Maybe (ConcreteVal tp) -> ConcreteVal tp -> IO OptionSetResult)
-> OptionStyle tp
-> OptionStyle tp
set_opt_onset f s = s { opt_onset = f }
set_opt_help :: Doc
-> OptionStyle tp
-> OptionStyle tp
set_opt_help v s = s { opt_help = v }
set_opt_default :: ConcreteVal tp
-> OptionStyle tp
-> OptionStyle tp
set_opt_default v s = s { opt_default_value = Just v }
data Bound r = Exclusive r
| Inclusive r
| Unbounded
boolOptSty :: OptionStyle BaseBoolType
boolOptSty = OptionStyle BaseBoolRepr
(\_ _ -> return optOK)
(text "Boolean")
Nothing
realOptSty :: OptionStyle BaseRealType
realOptSty = OptionStyle BaseRealRepr
(\_ _ -> return optOK)
(text "ℝ")
Nothing
integerOptSty :: OptionStyle BaseIntegerType
integerOptSty = OptionStyle BaseIntegerRepr
(\_ _ -> return optOK)
(text "ℤ")
Nothing
stringOptSty :: OptionStyle (BaseStringType Unicode)
stringOptSty = OptionStyle (BaseStringRepr UnicodeRepr)
(\_ _ -> return optOK)
(text "string")
Nothing
checkBound :: Ord a => Bound a -> Bound a -> a -> Bool
checkBound lo hi a = checkLo lo a && checkHi a hi
where checkLo Unbounded _ = True
checkLo (Inclusive x) y = x <= y
checkLo (Exclusive x) y = x < y
checkHi _ Unbounded = True
checkHi x (Inclusive y) = x <= y
checkHi x (Exclusive y) = x < y
docInterval :: Show a => Bound a -> Bound a -> Doc
docInterval lo hi = docLo lo <> text ", " <> docHi hi
where docLo Unbounded = text "(-∞"
docLo (Exclusive r) = text "(" <> text (show r)
docLo (Inclusive r) = text "[" <> text (show r)
docHi Unbounded = text "+∞)"
docHi (Exclusive r) = text (show r) <> text ")"
docHi (Inclusive r) = text (show r) <> text "]"
realWithRangeOptSty :: Bound Rational -> Bound Rational -> OptionStyle BaseRealType
realWithRangeOptSty lo hi = realOptSty & set_opt_onset vf
& set_opt_help help
where help = text "ℝ ∈" <+> docInterval lo hi
vf :: Maybe (ConcreteVal BaseRealType) -> ConcreteVal BaseRealType -> IO OptionSetResult
vf _ (ConcreteReal x)
| checkBound lo hi x = return optOK
| otherwise = return $ optErr $
text (show x) <+> text "out of range, expected real value in "
<+> docInterval lo hi
realWithMinOptSty :: Bound Rational -> OptionStyle BaseRealType
realWithMinOptSty lo = realWithRangeOptSty lo Unbounded
realWithMaxOptSty :: Bound Rational -> OptionStyle BaseRealType
realWithMaxOptSty hi = realWithRangeOptSty Unbounded hi
integerWithRangeOptSty :: Bound Integer -> Bound Integer -> OptionStyle BaseIntegerType
integerWithRangeOptSty lo hi = integerOptSty & set_opt_onset vf
& set_opt_help help
where help = text "ℤ ∈" <+> docInterval lo hi
vf :: Maybe (ConcreteVal BaseIntegerType) -> ConcreteVal BaseIntegerType -> IO OptionSetResult
vf _ (ConcreteInteger x)
| checkBound lo hi x = return optOK
| otherwise = return $ optErr $
text (show x) <+> text "out of range, expected integer value in "
<+> docInterval lo hi
integerWithMinOptSty :: Bound Integer -> OptionStyle BaseIntegerType
integerWithMinOptSty lo = integerWithRangeOptSty lo Unbounded
integerWithMaxOptSty :: Bound Integer -> OptionStyle BaseIntegerType
integerWithMaxOptSty hi = integerWithRangeOptSty Unbounded hi
enumOptSty :: Set Text -> OptionStyle (BaseStringType Unicode)
enumOptSty elts = stringOptSty & set_opt_onset vf
& set_opt_help help
where help = group (text "one of: " <+> align (sep $ map (dquotes . text . Text.unpack) $ Set.toList elts))
vf :: Maybe (ConcreteVal (BaseStringType Unicode))
-> ConcreteVal (BaseStringType Unicode)
-> IO OptionSetResult
vf _ (ConcreteString (UnicodeLiteral x))
| x `Set.member` elts = return optOK
| otherwise = return $ optErr $
text "invalid setting" <+> text (show x) <+>
text ", expected one of:" <+>
align (sep (map (text . Text.unpack) $ Set.toList elts))
listOptSty
:: Map Text (IO OptionSetResult)
-> OptionStyle (BaseStringType Unicode)
listOptSty values = stringOptSty & set_opt_onset vf
& set_opt_help help
where help = group (text "one of: " <+> align (sep $ map (dquotes . text . Text.unpack . fst) $ Map.toList values))
vf :: Maybe (ConcreteVal (BaseStringType Unicode))
-> ConcreteVal (BaseStringType Unicode)
-> IO OptionSetResult
vf _ (ConcreteString (UnicodeLiteral x)) =
fromMaybe
(return $ optErr $
text "invalid setting" <+> text (show x) <+>
text ", expected one of:" <+>
align (sep (map (text . Text.unpack . fst) $ Map.toList values)))
(Map.lookup x values)
executablePathOptSty :: OptionStyle (BaseStringType Unicode)
executablePathOptSty = stringOptSty & set_opt_onset vf
& set_opt_help help
where help = text "<path>"
vf :: Maybe (ConcreteVal (BaseStringType Unicode))
-> ConcreteVal (BaseStringType Unicode)
-> IO OptionSetResult
vf _ (ConcreteString (UnicodeLiteral x)) =
do me <- try (Env.findExecutable (Text.unpack x))
case me of
Right{} -> return $ optOK
Left e -> return $ optWarn $ text $ ioeGetErrorString e
data ConfigDesc where
ConfigDesc :: ConfigOption tp -> OptionStyle tp -> Maybe Doc -> ConfigDesc
mkOpt :: ConfigOption tp
-> OptionStyle tp
-> Maybe Doc
-> Maybe (ConcreteVal tp)
-> ConfigDesc
mkOpt o sty h def = ConfigDesc o sty{ opt_default_value = def } h
opt :: Pretty help
=> ConfigOption tp
-> ConcreteVal tp
-> help
-> ConfigDesc
opt o a help = mkOpt o (defaultOpt (configOptionType o))
(Just (pretty help))
(Just a)
optV :: forall tp help
. Pretty help
=> ConfigOption tp
-> ConcreteVal tp
-> (ConcreteVal tp -> Maybe help)
-> help
-> ConfigDesc
optV o a vf h = mkOpt o (defaultOpt (configOptionType o)
& set_opt_onset onset)
(Just (pretty h))
(Just a)
where onset :: Maybe (ConcreteVal tp) -> ConcreteVal tp -> IO OptionSetResult
onset _ x = case vf x of
Nothing -> return optOK
Just z -> return $ optErr $ pretty z
optU :: Pretty help
=> ConfigOption tp
-> help
-> ConfigDesc
optU o h = mkOpt o (defaultOpt (configOptionType o)) (Just (pretty h)) Nothing
optUV :: forall help tp.
Pretty help =>
ConfigOption tp ->
(ConcreteVal tp -> Maybe help) ->
help ->
ConfigDesc
optUV o vf h = mkOpt o (defaultOpt (configOptionType o)
& set_opt_onset onset)
(Just (pretty h))
Nothing
where onset :: Maybe (ConcreteVal tp) -> ConcreteVal tp -> IO OptionSetResult
onset _ x = case vf x of
Nothing -> return optOK
Just z -> return $ optErr $ pretty z
data ConfigLeaf where
ConfigLeaf ::
!(OptionStyle tp) ->
IORef (Maybe (ConcreteVal tp)) ->
Maybe Doc ->
ConfigLeaf
data ConfigTrie where
ConfigTrie ::
!(Maybe ConfigLeaf) ->
!ConfigMap ->
ConfigTrie
type ConfigMap = Map Text ConfigTrie
freshLeaf :: [Text] -> ConfigLeaf -> ConfigTrie
freshLeaf [] l = ConfigTrie (Just l) mempty
freshLeaf (a:as) l = ConfigTrie Nothing (Map.singleton a (freshLeaf as l))
adjustConfigTrie :: Functor t => [Text] -> (Maybe ConfigLeaf -> t (Maybe ConfigLeaf)) -> Maybe (ConfigTrie) -> t (Maybe ConfigTrie)
adjustConfigTrie as f Nothing = fmap (freshLeaf as) <$> f Nothing
adjustConfigTrie (a:as) f (Just (ConfigTrie x m)) = Just . ConfigTrie x <$> adjustConfigMap a as f m
adjustConfigTrie [] f (Just (ConfigTrie x m)) = g <$> f x
where g Nothing | Map.null m = Nothing
g x' = Just (ConfigTrie x' m)
adjustConfigMap :: Functor t => Text -> [Text] -> (Maybe ConfigLeaf -> t (Maybe ConfigLeaf)) -> ConfigMap -> t ConfigMap
adjustConfigMap a as f = Map.alterF (adjustConfigTrie as f) a
traverseConfigMap ::
Applicative t =>
[Text] ->
([Text] -> ConfigLeaf -> t ConfigLeaf) ->
ConfigMap ->
t ConfigMap
traverseConfigMap revPath f = Map.traverseWithKey (\k -> traverseConfigTrie (k:revPath) f)
traverseConfigTrie ::
Applicative t =>
[Text] ->
([Text] -> ConfigLeaf -> t ConfigLeaf) ->
ConfigTrie ->
t ConfigTrie
traverseConfigTrie revPath f (ConfigTrie x m) =
ConfigTrie <$> traverse (f (reverse revPath)) x <*> traverseConfigMap revPath f m
traverseSubtree ::
Applicative t =>
[Text] ->
([Text] -> ConfigLeaf -> t ConfigLeaf) ->
ConfigMap ->
t ConfigMap
traverseSubtree ps0 f = go ps0 []
where
go [] revPath = traverseConfigMap revPath f
go (p:ps) revPath = Map.alterF (traverse g) p
where g (ConfigTrie x m) = ConfigTrie x <$> go ps (p:revPath) m
insertOption :: (MonadIO m, MonadFail m) => ConfigDesc -> ConfigMap -> m ConfigMap
insertOption (ConfigDesc (ConfigOption _tp (p:|ps)) sty h) m = adjustConfigMap p ps f m
where
f Nothing =
do ref <- liftIO (newIORef (opt_default_value sty))
return (Just (ConfigLeaf sty ref h))
f (Just _) = fail ("Option " ++ showPath ++ " already exists")
showPath = Text.unpack (Text.intercalate "." (p:ps))
newtype Config = Config (IORef ConfigMap)
initialConfig :: Integer
-> [ConfigDesc]
-> IO (Config)
initialConfig initVerbosity ts = do
cfg <- Config <$> newIORef Map.empty
extendConfig (builtInOpts initVerbosity ++ ts) cfg
return cfg
extendConfig :: [ConfigDesc]
-> Config
-> IO ()
extendConfig ts (Config cfg) =
(readIORef cfg >>= \m -> foldM (flip insertOption) m ts) >>= writeIORef cfg
verbosity :: ConfigOption BaseIntegerType
verbosity = configOption BaseIntegerRepr "verbosity"
builtInOpts :: Integer -> [ConfigDesc]
builtInOpts initialVerbosity =
[ opt verbosity
(ConcreteInteger initialVerbosity)
(text "Verbosity of the simulator: higher values produce more detailed informational and debugging output.")
]
verbosityLogger :: Config -> Handle -> IO (Int -> String -> IO ())
verbosityLogger cfg h =
do verb <- getOptionSetting verbosity cfg
return $ \n msg ->
do v <- getOpt verb
when (toInteger n < v) (hPutStr h msg)
class Opt (tp :: BaseType) (a :: Type) | tp -> a where
getMaybeOpt :: OptionSetting tp -> IO (Maybe a)
trySetOpt :: OptionSetting tp -> a -> IO OptionSetResult
setOpt :: OptionSetting tp -> a -> IO [Doc]
setOpt x v = trySetOpt x v >>= checkOptSetResult
getOpt :: OptionSetting tp -> IO a
getOpt x = maybe (fail msg) return =<< getMaybeOpt x
where msg = "Option is not set: " ++ show (optionSettingName x)
checkOptSetResult :: OptionSetResult -> IO [Doc]
checkOptSetResult res =
case optionSetError res of
Just msg -> fail (show msg)
Nothing -> return (toList (optionSetWarnings res))
instance Opt (BaseStringType Unicode) Text where
getMaybeOpt x = fmap (fromUnicodeLit . fromConcreteString) <$> getOption x
trySetOpt x v = setOption x (ConcreteString (UnicodeLiteral v))
instance Opt BaseNatType Natural where
getMaybeOpt x = fmap fromConcreteNat <$> getOption x
trySetOpt x v = setOption x (ConcreteNat v)
instance Opt BaseIntegerType Integer where
getMaybeOpt x = fmap fromConcreteInteger <$> getOption x
trySetOpt x v = setOption x (ConcreteInteger v)
instance Opt BaseBoolType Bool where
getMaybeOpt x = fmap fromConcreteBool <$> getOption x
trySetOpt x v = setOption x (ConcreteBool v)
getOptionSetting ::
ConfigOption tp ->
Config ->
IO (OptionSetting tp)
getOptionSetting o@(ConfigOption tp (p:|ps)) (Config cfg) =
getConst . adjustConfigMap p ps f =<< readIORef cfg
where
f Nothing = Const (fail $ "Option not found: " ++ show o)
f (Just x) = Const (leafToSetting x)
leafToSetting (ConfigLeaf sty ref _h)
| Just Refl <- testEquality (opt_type sty) tp = return $
OptionSetting
{ optionSettingName = o
, getOption = readIORef ref
, setOption = \v ->
do old <- readIORef ref
res <- opt_onset sty old v
unless (isJust (optionSetError res)) (writeIORef ref (Just v))
return res
}
| otherwise = fail ("Type mismatch retriving option " ++ show o ++
"\nExpected: " ++ show tp ++ " but found " ++ show (opt_type sty))
getOptionSettingFromText ::
Text ->
Config ->
IO (Some OptionSetting)
getOptionSettingFromText nm (Config cfg) =
case splitPath nm of
Nothing -> fail "Illegal empty name for option"
Just (p:|ps) -> getConst . adjustConfigMap p ps (f (p:|ps)) =<< readIORef cfg
where
f (p:|ps) Nothing = Const (fail $ "Option not found: " ++ (Text.unpack (Text.intercalate "." (p:ps))))
f path (Just x) = Const (leafToSetting path x)
leafToSetting path (ConfigLeaf sty ref _h) = return $
Some OptionSetting
{ optionSettingName = ConfigOption (opt_type sty) path
, getOption = readIORef ref
, setOption = \v ->
do old <- readIORef ref
res <- opt_onset sty old v
unless (isJust (optionSetError res)) (writeIORef ref (Just v))
return res
}
data ConfigValue where
ConfigValue :: ConfigOption tp -> ConcreteVal tp -> ConfigValue
getConfigValues ::
Text ->
Config ->
IO [ConfigValue]
getConfigValues prefix (Config cfg) =
do m <- readIORef cfg
let ps = Text.splitOn "." prefix
f :: [Text] -> ConfigLeaf -> WriterT (Seq ConfigValue) IO ConfigLeaf
f [] _ = fail $ "getConfigValues: illegal empty option name"
f (p:path) l@(ConfigLeaf sty ref _h) =
do liftIO (readIORef ref) >>= \case
Just x -> tell (Seq.singleton (ConfigValue (ConfigOption (opt_type sty) (p:|path)) x))
Nothing -> return ()
return l
toList <$> execWriterT (traverseSubtree ps f m)
ppSetting :: [Text] -> Maybe (ConcreteVal tp) -> Doc
ppSetting nm v = fill 30 (text (Text.unpack (Text.intercalate "." nm))
<> maybe empty (\x -> text " = " <> ppConcrete x) v
)
ppOption :: [Text] -> OptionStyle tp -> Maybe (ConcreteVal tp) -> Maybe Doc -> Doc
ppOption nm sty x help =
group (ppSetting nm x <//> indent 2 (opt_help sty)) <$$> maybe empty (indent 2) help
ppConfigLeaf :: [Text] -> ConfigLeaf -> IO Doc
ppConfigLeaf nm (ConfigLeaf sty ref help) =
do x <- readIORef ref
return $ ppOption nm sty x help
configHelp ::
Text ->
Config ->
IO [Doc]
configHelp prefix (Config cfg) =
do m <- readIORef cfg
let ps = Text.splitOn "." prefix
f :: [Text] -> ConfigLeaf -> WriterT (Seq Doc) IO ConfigLeaf
f nm leaf = do d <- liftIO (ppConfigLeaf nm leaf)
tell (Seq.singleton d)
return leaf
toList <$> (execWriterT (traverseSubtree ps f m))